Claude Code × TanStack Query:キャッシュ戦略から楽観的更新まで効率的に実装する

Claude Code × TanStack Query:キャッシュ戦略から楽観的更新まで効率的に実装する

はじめに

「APIから取得したデータをどこで管理するか」という問いに、長年のReact開発者でも迷う場面がある。useStateで管理すれば再フェッチのタイミングを自分で制御しなければならない。ReduxやZustandに入れれば、本来クライアント固有のUIの状態とサーバーデータが混在し始める。

TanStack Query(旧React Query)は、この問いに対して明快な設計思想を提示する。「サーバーから取得するデータ」と「UIのローカル状態」は根本的に異なるものであり、異なる道具で管理すべきだ、という考え方だ。この分離をClaude Codeに伝えるための規約をCLAUDE.mdに書いておくと、APIフェッチのコードが格段に一貫したものになる。


TanStack Queryとは——クライアント状態とサーバー状態の分離

TanStack QueryはReactアプリケーションのサーバー状態管理ライブラリだ。@tanstack/react-queryパッケージとして提供され、v5が現在の最新メジャーバージョンになっている。

設計の核心は、状態の性質を2つに分けることだ。

クライアント状態はUIの開閉・フォーム入力値・選択状態といった、アプリケーション固有のローカルな状態を指す。ZustandやJotai、useState で管理する領域だ。サーバー状態はAPIから取得するデータ、そのキャッシュ、非同期フェッチを含む。TanStack Queryはこちらを担う。

この分離を徹底することで、「APIから取得したデータをReduxストアにコピーして管理する」という、キャッシュの無効化タイミングに悩みがちなパターンから解放される。

v5での主な変更点として、useQueryの引数が単一オブジェクト形式になり(v4まではpositional引数)、onSuccess/onErrorコールバックがuseQueryから廃止されている(useMutationには残存)。cacheTimegcTimeにリネームされた。


CLAUDE.mdでクエリキー規約を定義する

Claude Codeが一貫したTanStack Queryコードを生成するために、最も効果的な準備はCLAUDE.mdへの規約記述だ。

## TanStack Query 規約

- サーバー状態は必ず TanStack Query で管理する(直接 fetch() を書かない)
- useQuery / useMutation のみ使用

### queryKey 設計規約
- queryKey は配列階層構造で設計する
  - ['todos'] → todos 全体を無効化
  - ['todos', 'list', { status: 'active' }] → フィルタ付きリスト
  - ['todos', 'detail', todoId] → 特定アイテム
- queryKey には queryFn で使う全変数を含める(dependency array として扱う)
- 型一貫性: ['todos', 1] と ['todos', '1'] は別キー。型を統一する

### キャッシュ設定デフォルト
- staleTime: 5分(5 * 60 * 1000)
- gcTime: 10分(10 * 60 * 1000)
- マスターデータは staleTime: Infinity

queryKeyの設計は、キャッシュの無効化範囲を決める。['todos']を無効化すれば全てのtodosクエリが更新され、['todos', 'detail', id]を無効化すれば特定のtodoだけが更新される。この階層構造をCLAUDE.mdに書いておくことで、Claude Codeが新しいクエリを追加する際にも一貫した命名規則が維持される。

staleTimegcTimeの違いも整理しておく。staleTimeはデータを「新鮮」とみなす期間で、この間は再フェッチが行われない。gcTimeは非アクティブなキャッシュをメモリから削除するまでの期間だ。デフォルトはstaleTime: 0(常に古いとみなす)・gcTime: 5分。マスターデータのような変化の少ないデータにはstaleTime: Infinityが適している。


Claude Codeへの具体的な指示パターン

useQueryの実装を依頼する

TanStack Query v5 で useQuery を実装してください。
- queryKey は ['todos', 'list'] の配列階層構造
- staleTime: 5分
- エラー時は ErrorBoundary に委譲(onError コールバックは使わない)
- queryFn は /api/todos に GET リクエスト

Claude Codeが生成するコード:

'use client'
import { useQuery } from '@tanstack/react-query'

type Todo = { id: number; text: string; done: boolean }

async function fetchTodos(): Promise<Todo[]> {
  const res = await fetch('/api/todos')
  if (!res.ok) throw new Error('Failed to fetch todos')
  return res.json()
}

export function TodoList() {
  const { data: todos, isLoading, error } = useQuery({
    queryKey: ['todos', 'list'],
    queryFn: fetchTodos,
    staleTime: 5 * 60 * 1000,
  })

  if (isLoading) return <div>読み込み中...</div>
  if (error) return <div>エラー: {error.message}</div>

  return (
    <ul>
      {todos?.map(todo => <li key={todo.id}>{todo.text}</li>)}
    </ul>
  )
}

楽観的更新を依頼する

楽観的更新は実装パターンが決まっているため、Claude Codeへの指示でパターン名を明示すると精度が上がる:

楽観的更新(Optimistic Update)を useMutation で実装してください。
パターン: キャッシュ経由(onMutate でスナップショット取得 →
setQueryData で即時反映 → onError でロールバック → onSettled で invalidate)
対象クエリキー: ['todos', 'list']

Claude Codeが生成するコード(コメント付き):

const toggleTodoMutation = useMutation({
  mutationFn: (id: number) =>
    fetch(/api/todos/${id}/toggle, { method: 'PATCH' }).then(r => r.json()),

  onMutate: async (id) => {
    // 進行中のクエリをキャンセル(上書き防止)
    await queryClient.cancelQueries({ queryKey: ['todos', 'list'] })
    // 現在のキャッシュを保存(ロールバック用)
    const previousTodos = queryClient.getQueryData<Todo[]>(['todos', 'list'])
    // キャッシュを楽観的に更新
    queryClient.setQueryData<Todo[]>(['todos', 'list'], (old) =>
      old?.map(todo =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      ) ?? []
    )
    return { previousTodos }
  },

  onError: (_err, _id, context) => {
    if (context?.previousTodos) {
      queryClient.setQueryData(['todos', 'list'], context.previousTodos)
    }
  },

  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos', 'list'] })
  },
})

queryOptionsで型安全を高める

v5で追加されたqueryOptionsを使うと、useQueryqueryClientで同じ型安全なオプションを共有できる:

// queries/todos.ts
import { queryOptions } from '@tanstack/react-query'

export const todoDetailOptions = (id: number) =>
  queryOptions({
    queryKey: ['todos', 'detail', id],
    queryFn: () => fetchTodo(id),
    staleTime: 5 * 60 * 1000,
  })

// 使用側(型安全)
const { data } = useQuery(todoDetailOptions(1))
queryClient.setQueryData(todoDetailOptions(42).queryKey, newTodo)

queryOptionsを使って型安全なクエリを定義してください」と指示するだけで、このパターンをClaude Codeが生成する。


Next.js App Routerとの統合

Server ComponentsでのSSRとクライアント側のキャッシュを組み合わせるHydrationBoundaryパターンは、Next.js App RouterとTanStack Queryを使う上でよく登場する:

// app/posts/page.tsx(Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'

export default async function PostsPage() {
  const queryClient = new QueryClient()

  // サーバー側でプリフェッチ
  await queryClient.prefetchQuery({
    queryKey: ['posts', 'list'],
    queryFn: getPosts,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <PostList />
    </HydrationBoundary>
  )
}

// app/posts/PostList.tsx(Client Component)
'use client'
export default function PostList() {
  // サーバーでプリフェッチされたデータがハイドレーションされて即座に表示
  const { data } = useQuery({
    queryKey: ['posts', 'list'],
    queryFn: getPosts,
  })
  // ...
}

サーバー側でプリフェッチされたデータがクライアントにハイドレーションされ、初期表示でローディング状態を回避できる。Claude Codeへの指示では「HydrationBoundaryパターンでServer ComponentからプリフェッチしてClient Componentに渡してください」という形が通りやすい。


まとめ

TanStack QueryをClaude Codeと一緒に使う際の実質的な出発点は、CLAUDE.mdに「サーバー状態はTanStack Queryで管理する」「queryKeyの設計規約はこの階層構造に従う」と書くことだ。この文脈を渡した状態でuseQueryの実装を依頼すれば、プロジェクト全体で一貫したキャッシュキーの命名とstaleTimeの設定が維持される。

楽観的更新・無限スクロール・Next.js SSRとの統合といった応用的なパターンも、指示の中でパターン名を明示することでClaude Codeが正確なコードを生成する。「TanStack Query v5のHydrationBoundaryパターンで」「楽観的更新のキャッシュ経由パターンで」——こうした前置きが、生成されるコードの質を決める。

コメント

タイトルとURLをコピーしました