mcpField
T
tanstack-query

tanstack-query

github.com/secondsky開発244304 回インストール5 週間前に更新
リポジトリ

tanstack-query について

React 18+ と TypeScript のプロジェクトで TanStack Query v5 を使うための開発スキルです。クエリ、ミューテーション、キャッシュ設定と移行時の注意点を扱います。

主な機能

  1. 01クエリとミューテーションの管理
  2. 02キャッシュと再取得の設定
  3. 03プリフェッチと無限クエリの利用
  4. 04DevTools とエラー処理の解説
  5. 05v4 から v5 への移行差分の説明

活用シーン

  1. 01React アプリへのサーバーデータ取得の導入
  2. 02ミューテーション後の関連クエリ更新
  3. 03ページングリストとデータのプリフェッチ
  4. 04v5 の状態フラグやオプションの問題調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/secondsky/claude-skills --skill tanstack-query

インストール後、Claude Code で/tanstack-queryを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

assets/
example-template.txt
examples/
basic-graphql-request.tsx
basic.tsx
default-query-function.tsx
infinite-scroll.tsx
nextjs-app-router.tsx
optimistic-update.tsx
pagination.tsx
prefetching.tsx
react-native.tsx
README.md
suspense.tsx
references/
advanced-setup.md
best-practices.md
common-patterns.md
configuration-files.md
example-reference.md
official-guides-map.md
testing.md
top-errors.md
typescript-patterns.md
v4-to-v5-migration.md
scripts/
example-script.sh
templates/
custom-hooks-pattern.tsx
devtools-setup.tsx
error-boundary.tsx
package.json
provider-setup.tsx
query-client-config.ts
use-infinite-query.tsx

詳細

TanStack Query (React Query) v5

ステータス:本番環境対応済み ✅ 最終更新日:2026-08-03 依存関係:React 18.0+(18.3+ を推奨)、TypeScript 4.9+(5.x を推奨) 最新バージョン:@tanstack/[email protected]、@tanstack/[email protected]、@tanstack/[email protected]


クイックスタート(5 分)

1. 依存関係をインストールする
# choose your package manager
pnpm add @tanstack/react-query@latest @tanstack/react-query-devtools@latest
# or
npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latest
# or
bun add @tanstack/react-query@latest @tanstack/react-query-devtools@latest

重要な理由:

  • TanStack Query v5 には React 18+ が必要です(useSyncExternalStore を使用します)
  • DevTools はクエリとミューテーションのデバッグに不可欠です
  • v5 には v4 からの破壊的変更があるため、修正がすべて含まれた最新版を使用してください
2. QueryClient Provider を設定する
// src/main.tsx or src/index.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import App from './App'

// Create a client
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      gcTime: 1000 * 60 * 60, // 1 hour (formerly cacheTime)
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
})

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  </StrictMode>
)

重要:

  • アプリ全体を QueryClientProvider でラップしてください
  • 過剰な再フェッチを避けるため、staleTime を設定してください(デフォルトは 0 です)
  • gcTime を使用してください(cacheTime ではありません。v5 で名称が変更されました)
  • DevTools は Provider の内側に配置してください

デフォルト値を把握する(v5):

  • staleTime: 0 → データはすぐに stale とみなされるため、値を大きくしない限り、マウント時やフォーカス時に再フェッチされます
  • gcTime: 5 * 60 * 1000 → 非アクティブなデータは 5 分後にガベージコレクションされます
  • ブラウザーでは retry: 3、サーバーでは retry: 0
  • refetchOnWindowFocus: true と refetchOnReconnect: true
  • networkMode: 'online'(オフライン中はリクエストが一時停止します)。キャンセルを避けたい SSR/プリフェッチでは 'always' に切り替えてください。 citeturn1search0turn1search1
3. 最初のクエリを作成する
// src/hooks/useTodos.ts
import { useQuery } from '@tanstack/react-query'

type Todo = {
  id: number
  title: string
  completed: boolean
}

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

export function useTodos() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
}

// Usage in component:
function TodoList() {
  const { data, isPending, isError, error } = useTodos()

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error: {error.message}</div>

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

重要:

  • v5 ではオブジェクト構文が必須です:useQuery({ queryKey, queryFn })
  • isPending を使用してください(isLoading ではありません。現在は「pending かつ fetching」を意味します)
  • 適切なエラー処理のため、queryFn では必ずエラーをスローしてください
  • 一貫したキャッシュキーを使えるよう、QueryKey は配列にしてください
4. 最初のミューテーションを作成する
// src/hooks/useAddTodo.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'

type NewTodo = {
  title: string
}

async function addTodo(newTodo: NewTodo) {
  const response = await fetch('/api/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(newTodo),
  })
  if (!response.ok) throw new Error('Failed to add todo')
  return response.json()
}

export function useAddTodo() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: addTodo,
    onSuccess: () => {
      // Invalidate and refetch todos
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })
}

// Usage in component:
function AddTodoForm() {
  const { mutate, isPending } = useAddTodo()

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
    mutate({ title: formData.get('title') as string })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Adding...' : 'Add Todo'}
      </button>
    </form>
  )
}

この方法が機能する理由:

  • ミューテーションではコールバック(onSuccess、onError、onSettled)を使用します。クエリでは使用しません
  • invalidateQueries はバックグラウンドでの再フェッチを開始します
  • ミューテーションはデフォルトではキャッシュされません(正しい動作です)

7 ステップのセットアップ手順

ステップ 1:依存関係をインストールする
# Core library (required)
pnpm add @tanstack/react-query

# DevTools (highly recommended for development)
pnpm add -D @tanstack/react-query-devtools

# Optional: ESLint plugin for best practices
pnpm add -D @tanstack/eslint-plugin-query

各パッケージの役割:

  • @tanstack/react-query - React のコアフックと QueryClient
  • @tanstack/react-query-devtools - 視覚的なデバッガー(開発時のみ、ツリーシェイキング可能)
  • @tanstack/eslint-plugin-query - よくあるミスを検出します

バージョン要件:

  • React 18.0 以降(useSyncExternalStore を使用します)
  • 最適な型推論には TypeScript 5.2+ が必要です(任意ですが推奨)
ステップ 2:QueryClient を設定する
// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // How long data is considered fresh (won't refetch during this time)
      staleTime: 1000 * 60 * 5, // 5 minutes

      // How long inactive data stays in cache before garbage collection
      gcTime: 1000 * 60 * 60, // 1 hour (v5: renamed from cacheTime)

      // Retry failed requests (0 on server, 3 on client by default)
      retry: (failureCount, error) => {
        if (error instanceof Response && error.status === 404) return false
        return failureCount < 3
      },

      // Refetch on window focus (can be annoying during dev)
      refetchOnWindowFocus: false,

      // Refetch on network reconnect
      refetchOnReconnect: true,

      // Refetch on component mount if data is stale
      refetchOnMount: true,
    },
    mutations: {
      // Retry mutations on failure (usually don't want this)
      retry: 0,
    },
  },
})

主な設定上の判断:

staleTime と gcTime:

  • staleTime:データが「stale」とみなされ、再フェッチされる可能性があるまでの時間
    • 0(デフォルト):データはすぐに stale となり、マウント時やフォーカス時に再フェッチされます
    • 1000 * 60 * 5:データは 5 分間 fresh となり、この間は再フェッチされません
    • Infinity:データは stale にならず、手動で無効化した場合のみ更新されます
  • gcTime:未使用のデータがキャッシュに保持される時間
    • 1000 * 60 * 5(デフォルト):5 分
    • Infinity:ガベージコレクションされません(メモリーリークのリスクがあります)

再フェッチするタイミング:

  • refetchOnWindowFocus: true - 頻繁に変化するデータ(株価など)に適しています
  • refetchOnWindowFocus: false - 安定したデータや開発中に適しています
  • refetchOnMount: true - コンポーネントのマウント時に最新データを取得します
  • refetchOnReconnect: true - ネットワークへの再接続後に再フェッチします
ステップ 3:Provider でアプリをラップする
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClient } from './lib/query-client'
import App from './App'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools
        initialIsOpen={false}
        buttonPosition="bottom-right"
      />
    </QueryClientProvider>
  </StrictMode>
)

Provider の配置:

  • TanStack Query のフックを使用するすべてのコンポーネントをラップする必要があります
  • DevTools は Provider の内側に配置する必要があります
  • アプリ全体で QueryClient インスタンスは 1 つだけにしてください

DevTools の設定:

  • initialIsOpen={false} - デフォルトで折りたたんだ状態にします
  • buttonPosition="bottom-right" - 切り替えボタンの表示位置を指定します
  • 本番ビルドでは自動的に削除されます(ツリーシェイキング)
高度なセットアップ(ステップ 4~7)

詳細なパターンについて:カスタムクエリフック、楽観的更新を伴うミューテーション、DevTools の設定、エラーバウンダリーを実装する場合は、references/advanced-setup.md を読み込んでください。

簡単な概要:

ステップ 4:カスタムクエリフック - 再利用可能なパターンには queryOptions ファクトリーを使用します。API 呼び出しをカプセル化するカスタムフックを作成します。

ステップ 5:ミューテーション - onSuccess を指定した useMutation を使用して、クエリを無効化します。UI に即時にフィードバックを反映するには、onMutate/onError/onSettled のパターンを使って楽観的更新を実装します。

ステップ 6:DevTools - ステップ 3 にすでに含まれています。カスタマイズ用の高度なオプションはリファレンスで確認できます。

ステップ 7:エラーバウンダリー - React エラーバウンダリーと QueryErrorResetBoundary を使用します。グローバルとローカルのどちらでエラーを処理するかに応じて、throwOnError オプションを設定します。


重要なルール

必ず行うこと

✅ すべてのフックでオブジェクト構文を使用する

// v5 ONLY supports this:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })

✅ クエリキーに配列を使用する

queryKey: ['todos']              // List
queryKey: ['todos', id]          // Detail
queryKey: ['todos', { filter }]  // Filtered

✅ staleTime を適切に設定する

staleTime: 1000 * 60 * 5 // 5 min - prevents excessive refetches

✅ 初期ローディング状態には isPending を使用する

if (isPending) return <Loading />
// isPending = no data yet AND fetching

✅ queryFn でエラーをスローする

if (!response.ok) throw new Error('Failed')

✅ ミューテーション後にクエリを無効化する

onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['todos'] })
}

✅ 再利用可能なパターンには queryOptions ファクトリーを使用する

const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)

✅ gcTime を使用する(cacheTime は使用しない)

gcTime: 1000 * 60 * 60 // 1 hour

✅ ステータスフラグを把握する

isPending      // no data yet, fetch in flight
isFetching     // any fetch in flight (including refetch)
isRefetching   // refetch specifically (data already cached)
isLoadingError // initial load failed
isPaused       // networkMode paused (e.g., offline)
isFetchingNextPage // useInfiniteQuery loading more
決して行わないこと

❌ v4 の配列/関数構文を決して使用しない

// v4 (removed in v5):
useQuery(['todos'], fetchTodos, options) // ❌

// v5 (correct):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅

❌ クエリコールバック(クエリ内の onSuccess、onError、onSettled)を決して使用しない

// v5 removed these from queries:
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  onSuccess: (data) => {}, // ❌ Removed in v5
})

// Use useEffect instead:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    // Do something
  }
}, [data])

// Or use mutation callbacks (still supported):
useMutation({
  mutationFn: addTodo,
  onSuccess: () => {}, // ✅ Still works for mutations
})

❌ 非推奨のオプションを決して使用しない

// Deprecated in v5:
cacheTime: 1000 // ❌ Use gcTime instead
isLoading: true // ❌ Meaning changed, use isPending
keepPreviousData: true // ❌ Use placeholderData instead
onSuccess: () => {} // ❌ Removed from queries
useErrorBoundary: true // ❌ Use throwOnError instead

❌ isLoading が「まだデータがない」ことを意味すると決して思い込まない

// v5 changed this:
isLoading = isPending && isFetching // ❌ Now means "pending AND fetching"
isPending = no data yet // ✅ Use this for initial load

❌ 無限クエリで initialPageParam を決して忘れない

// v5 requires this:
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // ✅ Required in v5
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

❌ useSuspenseQuery で enabled を決して使用しない

// Not allowed:
useSuspenseQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  enabled: !!id, // ❌ Not available with suspense
})

// Use conditional rendering instead:
{id && <TodoComponent id={id} />}

エラーの予防

このスキルは、文書化されている v5 移行時の問題を 8 件以上防ぎます。特に重要なエラーは次のとおりです。

  • オブジェクト構文が必須(v4 の関数オーバーロードは削除)
  • クエリコールバックを削除(onSuccess/onError/onSettled)
  • isPending と isLoading のステータスの違い
  • cacheTime を gcTime に改名
  • 無限クエリで initialPageParam が必須
  • keepPreviousData を placeholderData に置き換え

エラー一覧と修正前/修正後の例について:エラーが発生した場合や v5 移行の問題をデバッグする場合は、references/top-errors.md を読み込んでください。


プロジェクト設定

必須の設定ファイル:package.json、tsconfig.json、.eslintrc.cjs

主な要件:

  • React 18.3.1 以降(useSyncExternalStore を使用)
  • TypeScript の strict モードを推奨
  • ESLint プラグインが v4 から v5 への移行エラーを検出

設定テンプレートの全容について:新しいプロジェクトをセットアップする場合や、ビルド/型エラーをトラブルシューティングする場合は、references/configuration-files.md を読み込んでください。


よく使うパターン

パターン 1:依存クエリ
// Fetch user, then fetch user's posts
function UserPosts({ userId }: { userId: number }) {
  const { data: user } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
  })

  const { data: posts } = useQuery({
    queryKey: ['users', userId, 'posts'],
    queryFn: () => fetchUserPosts(userId),
    enabled: !!user, // Only fetch posts after user is loaded
  })

  if (!user) return <div>Loading user...</div>
  if (!posts) return <div>Loading posts...</div>

  return (
    <div>
      <h1>{user.name}</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

使用する場面:クエリ B がクエリ A のデータに依存する場合

パターン 2:useQueries による並列クエリ
// Fetch multiple todos in parallel
function TodoDetails({ ids }: { ids: number[] }) {
  const results = useQueries({
    queries: ids.map(id => ({
      queryKey: ['todos', id],
      queryFn: () => fetchTodo(id),
    })),
  })

  const isLoading = results.some(result => result.isPending)
  const isError = results.some(result => result.isError)

  if (isLoading) return <div>Loading...</div>
  if (isError) return <div>Error loading todos</div>

  return (
    <ul>
      {results.map((result, i) => (
        <li key={ids[i]}>{result.data?.title}</li>
      ))}
    </ul>
  )
}

使用する場面:互いに独立した複数のクエリを並行して取得する場合

パターン 3:プリフェッチ
import { useQueryClient } from '@tanstack/react-query'
import { todosQueryOptions } from './hooks/useTodos'

function TodoListWithPrefetch() {
  const queryClient = useQueryClient()
  const { data: todos } = useTodos()

  const prefetchTodo = (id: number) => {
    queryClient.prefetchQuery({
      queryKey: ['todos', id],
      queryFn: () => fetchTodo(id),
      staleTime: 1000 * 60 * 5, // 5 minutes
    })
  }

  return (
    <ul>
      {todos?.map(todo => (
        <li
          key={todo.id}
          onMouseEnter={() => prefetchTodo(todo.id)}
        >
          <Link to={`/todos/${todo.id}`}>{todo.title}</Link>
        </li>
      ))}
    </ul>
  )
}

使用する場面:ユーザーが移動する前(ホバー時、マウント時)にデータを事前読み込みする場合

パターン 4:無限スクロール
import { useInfiniteQuery } from '@tanstack/react-query'
import { useEffect, useRef } from 'react'

type Page = {
  data: Todo[]
  nextCursor: number | null
}

async function fetchTodosPage({ pageParam }: { pageParam: number }): Promise<Page> {
  const response = await fetch(`/api/todos?cursor=${pageParam}&limit=20`)
  return response.json()
}

function InfiniteTodoList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['todos', 'infinite'],
    queryFn: fetchTodosPage,
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  })

  const loadMoreRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage) {
          fetchNextPage()
        }
      },
      { threshold: 0.1 }
    )

    if (loadMoreRef.current) {
      observer.observe(loadMoreRef.current)
    }

    return () => observer.disconnect()
  }, [fetchNextPage, hasNextPage])

  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.data.map(todo => (
            <div key={todo.id}>{todo.title}</div>
          ))}
        </div>
      ))}

      <div ref={loadMoreRef}>
        {isFetchingNextPage && <div>Loading more...</div>}
      </div>
    </div>
  )
}

使用する場面:無限スクロールを使ったページネーション付きリスト

パターン 5:クエリのキャンセル
function SearchTodos() {
  const [search, setSearch] = useState('')

  const { data } = useQuery({
    queryKey: ['todos', 'search', search],
    queryFn: async ({ signal }) => {
      const response = await fetch(`/api/todos?q=${search}`, { signal })
      return response.json()
    },
    enabled: search.length > 2, // Only search if 3+ characters
  })

  return (
    <div>
      <input
        value={search}
        onChange={e => setSearch(e.target.value)}
        placeholder="Search todos..."
      />
      {data && (
        <ul>
          {data.map(todo => (
            <li key={todo.id}>{todo.title}</li>
          ))}
        </ul>
      )}
    </div>
  )
}

動作の仕組み:

  • queryKey が変わると、前のクエリは自動的にキャンセルされます
  • 適切にクリーンアップするには、signal を fetch に渡します
  • ブラウザーが保留中の fetch リクエストを中止します
パターン 6:バックグラウンド取得のインジケーター
const { data, isFetching, isRefetching } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 1000 * 60 * 5,
})

return (
  <div>
    {isFetching && <Spinner label={isRefetching ? 'Refreshing…' : 'Loading…'} />}
    <TodoList data={data} />
  </div>
)

理由: バックグラウンドでの再取得中も isFetching は true のままなので、キャッシュされたデータを失わずに、控えめな「更新中」バッジを表示できます。


バンドルされたリソースの使用

テンプレート(templates/)

そのままコピーして使えるコード例です。

  • package.json - 正確なバージョンを指定した依存関係
  • query-client-config.ts - ベストプラクティスに沿った QueryClient の設定
  • provider-setup.tsx - QueryClientProvider を使ったアプリのラッパー
  • use-query-basic.tsx - 基本的な useQuery フックのパターン
  • use-mutation-basic.tsx - 基本的な useMutation フック
  • use-mutation-optimistic.tsx - 楽観的更新のパターン
  • use-infinite-query.tsx - 無限スクロールのパターン
  • custom-hooks-pattern.tsx - queryOptions を使った再利用可能なクエリフック
  • error-boundary.tsx - クエリのリセットに対応したエラーバウンダリ
  • devtools-setup.tsx - DevTools の設定

使用例:

# Copy query client config
cp ~/.claude/skills/tanstack-query/templates/query-client-config.ts src/lib/

# Copy provider setup
cp ~/.claude/skills/tanstack-query/templates/provider-setup.tsx src/main.tsx

# Or run the bootstrap helper (installs deps + copies core files):
./scripts/example-script.sh . pnpm
リファレンス(references/)

必要に応じて読み込む、詳細なドキュメントです。

  • advanced-setup.md - カスタムフック、ミューテーション、楽観的更新、DevTools、エラーバウンダリ
  • configuration-files.md - 完全な package.json、tsconfig.json、.eslintrc.cjs のテンプレート
  • v4-to-v5-migration.md - v4 → v5 の完全な移行ガイド
  • best-practices.md - リクエストのウォーターフォール、キャッシュ戦略、パフォーマンス
  • common-patterns.md - 再利用可能なクエリ、楽観的更新、無限スクロール
  • official-guides-map.md - 各公式ドキュメントを開くタイミングと内容
  • typescript-patterns.md - 型安全性、ジェネリクス、型推論
  • testing.md - MSW、React Testing Library を使ったテスト
  • top-errors.md - 解決策付きの 8 件以上のエラー
例(examples/)
  • examples/README.md - 公式リンク付きの主要な 10 個のシナリオのインデックス
  • basic.tsx - 最小限のリストクエリ
  • basic-graphql-request.tsx - GraphQL クライアント + select
  • optimistic-update.tsx - onMutate によるスナップショット/ロールバック
  • pagination.tsx - placeholderData を使ったページ分割リスト
  • infinite-scroll.tsx - useInfiniteQuery + IntersectionObserver
  • prefetching.tsx - ナビゲーション前にホバー時にプリフェッチ
  • suspense.tsx - useSuspenseQuery + バウンダリ
  • default-query-function.tsx - queryKey を使ったグローバルフェッチャー
  • nextjs-app-router.tsx - App Router のプリフェッチ + ハイドレーション(networkMode: 'always')
  • react-native.tsx - AsyncStorage persister を使ったオフライン優先の実装

Claude が読み込むタイミング:

  • advanced-setup.md - カスタムクエリフック、ミューテーション、またはエラーバウンダリを実装するとき
  • configuration-files.md - 新しいプロジェクトをセットアップするとき、またはビルド/型エラーをトラブルシューティングするとき
  • v4-to-v5-migration.md - 既存の React Query v4 プロジェクトを移行するとき
  • best-practices.md - パフォーマンスを最適化するとき、またはウォーターフォールを回避するとき
  • common-patterns.md - 特定の機能(無限スクロールなど)を実装するとき
  • typescript-patterns.md - TypeScript のエラーや型推論に対処するとき
  • testing.md - TanStack Query を使うコンポーネントのテストを書くとき
  • top-errors.md - メインの SKILL.md に記載されていないエラーが発生したとき

高度なトピック

select によるデータ変換
// Only subscribe to specific slice of data
function TodoCount() {
  const { data: count } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: (data) => data.length, // Only re-render when count changes
  })

  return <div>Total todos: {count}</div>
}

// Transform data shape
function CompletedTodoTitles() {
  const { data: titles } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: (data) =>
      data
        .filter(todo => todo.completed)
        .map(todo => todo.title),
  })

  return (
    <ul>
      {titles?.map((title, i) => (
        <li key={i}>{title}</li>
      ))}
    </ul>
  )
}

利点:

  • 選択したデータが変更されたときだけ、コンポーネントが再レンダリングされます
  • メモリ使用量を削減します(コンポーネントの状態に保存するデータが少なくなります)
  • クエリキャッシュを変更しません(ほかのコンポーネントは完全なデータを取得できます)
リクエストのウォーターフォール(アンチパターン)
// ❌ BAD: Sequential waterfalls
function BadUserProfile({ userId }: { userId: number }) {
  const { data: user } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
  })

  const { data: posts } = useQuery({
    queryKey: ['posts', user?.id],
    queryFn: () => fetchPosts(user!.id),
    enabled: !!user,
  })

  const { data: comments } = useQuery({
    queryKey: ['comments', posts?.[0]?.id],
    queryFn: () => fetchComments(posts![0].id),
    enabled: !!posts && posts.length > 0,
  })

  // Each query waits for previous one = slow!
}

// ✅ GOOD: Fetch in parallel when possible
function GoodUserProfile({ userId }: { userId: number }) {
  const { data: user } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
  })

  // Fetch posts AND comments in parallel
  const { data: posts } = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPosts(userId), // Don't wait for user
  })

  const { data: comments } = useQuery({
    queryKey: ['comments', userId],
    queryFn: () => fetchUserComments(userId), // Don't wait for posts
  })

  // All 3 queries run in parallel = fast!
}
サーバー状態とクライアント状態
// ❌ Don't use TanStack Query for client-only state
const { data: isModalOpen, setData: setIsModalOpen } = useMutation(...)

// ✅ Use useState for client state
const [isModalOpen, setIsModalOpen] = useState(false)

// ✅ Use TanStack Query for server state
const { data: todos } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

目安:

  • サーバー状態:TanStack Query を使います(API からのデータ)
  • クライアント状態:useState/useReducer を使います(ローカル UI の状態)
  • グローバルなクライアント状態:Zustand/Context を使います(テーマ、認証トークン)

プラットフォームとインテグレーションに関する注意事項

  • React Native:Web と同じように動作します。@tanstack/query-async-storage-persister を使ってキャッシュを AsyncStorage に永続化し、ウィンドウフォーカス時の再フェッチ処理は避けてください。DevTools パネルはネイティブでは利用できないため、Flipper を使うか、ログを公開してください。
  • GraphQL:graphql-request または urql のベアクライアントと組み合わせてください。オペレーションは通常の非同期関数として扱い、フラグメントを同じ場所にまとめ、select を使ってエッジ/ノードをフラットな形に変換してください。
  • SSR/Next.js/TanStack Start:サーバーでは dehydrate/HydrationBoundary を使い、クライアントでは QueryClientProvider を使ってください。サーバーでのプリフェッチでは、リクエストが一時停止されないように networkMode: 'always' を設定してください。
  • Suspense:すでに Suspense を使っているルートでは useSuspenseQuery を優先してください。enabled と併用せず、代わりにレンダリングを制御してください。
  • テスト:@testing-library/react と @tanstack/react-query/testing のヘルパーを使い、MSW でネットワークをモックしてください。キャッシュの漏れを防ぐため、テストごとに QueryClient をリセットしてください。

依存関係

必須:

推奨:

任意:

  • @tanstack/query-sync-storage-persister - キャッシュを localStorage に永続化
  • @tanstack/query-async-storage-persister - AsyncStorage に永続化(React Native)

公式ドキュメント


パッケージバージョン(2026-08-03 確認済み)

{
  "dependencies": {
    "@tanstack/react-query": "^5.101.4"
  },
  "devDependencies": {
    "@tanstack/react-query-devtools": "^5.101.4",
    "@tanstack/eslint-plugin-query": "^5.101.4"
  }
}

確認方法:

  • npm view @tanstack/react-query version → 5.101.4
  • npm view @tanstack/react-query-devtools version → 5.101.4
  • npm view @tanstack/eslint-plugin-query version → 5.101.4
  • 最終確認日:2026-08-03

本番環境での例

このスキルは、次のような本番環境で使用されているパターンに基づいています。

  • 構築時間:調査と開発に約 6 時間
  • 防止したエラー:8 件(v5 の移行に関する問題をすべて文書化)
  • トークン効率:手動セットアップと比べて約 65%削減
  • 検証:✅ すべてのパターンを TypeScript strict モードでテスト済み

トラブルシューティング

問題:「useQuery is not a function」または型エラーが発生する

解決策:v5 のオブジェクト構文を使っていることを確認してください。

// ✅ Correct:
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

// ❌ Wrong (v4 syntax):
useQuery(['todos'], fetchTodos)
問題:クエリでコールバック(onSuccess、onError)が動作しない

解決策:v5 では削除されました。useEffect を使うか、ミューテーションに移してください。

// ✅ For queries:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    // Handle success
  }
}, [data])

// ✅ For mutations (still work):
useMutation({
  mutationFn: addTodo,
  onSuccess: () => { /* ... */ },
})
問題:初回読み込み中でも isLoading が常に false になる

解決策:代わりに isPending を使ってください。

const { isPending, isLoading, isFetching } = useQuery(...)
// isPending = no data yet
// isLoading = isPending && isFetching
// isFetching = any fetch in progress
問題:cacheTime オプションが認識されない

解決策:v5 では gcTime に名称が変更されました。

gcTime: 1000 * 60 * 60 // 1 hour
問題:useSuspenseQuery で enabled オプションを使うと型エラーが発生する

解決策:suspense では enabled を使えません。条件付きレンダリングを使ってください。

{id && <TodoComponent id={id} />}
問題:ミューテーション後にデータが再フェッチされない

解決策:onSuccess でクエリを無効化してください。

onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['todos'] })
}
問題:無限クエリで initialPageParam が必要になる

解決策:v5 では必ず initialPageParam を指定してください。

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // Required
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
問題:keepPreviousData が動作しない

解決策:placeholderData に置き換えられました。

import { keepPreviousData } from '@tanstack/react-query'

useQuery({
  queryKey: ['todos', page],
  queryFn: () => fetchTodos(page),
  placeholderData: keepPreviousData,
})

セットアップ完了チェックリスト

このチェックリストを使って、セットアップを確認してください。

  • @tanstack/[email protected]+ をインストールした
  • @tanstack/react-query-devtools をインストールした(開発依存関係)
  • デフォルト設定を指定して QueryClient を作成した
  • QueryClientProvider でアプリをラップした
  • ReactQueryDevtools コンポーネントを追加した
  • オブジェクト構文を使って最初のクエリを作成した
  • isPending とエラー状態をテストした
  • onSuccess ハンドラーを使って最初のミューテーションを作成した
  • ミューテーション後のクエリ無効化を設定した
  • staleTime と gcTime を適切に設定した
  • 配列の queryKey を一貫して使用している
  • queryFn でエラーをスローしている
  • v4 構文(関数のオーバーロード)を使用していない
  • クエリコールバック(クエリでの onSuccess、onError)を使用していない
  • 初回読み込みに isLoading ではなく isPending を使用している
  • 開発環境で DevTools が動作している
  • TypeScript の型が正しく動作している
  • 本番用ビルドが成功する

質問や問題がありますか?

  1. エラーの解決方法については references/top-errors.md を確認してください
  2. セットアップ手順のすべてのステップを確認してください
  3. 公式ドキュメントを確認してください:https://tanstack.com/query/latest
  4. v5 構文(オブジェクト構文、gcTime、isPending)を使用していることを確認してください
  5. Discord に参加してください:https://tlinz.com/discord

コメント