mcpField
T
tanstack-query

tanstack-query

github.com/jackspace開発1652 回インストール13 週間前に更新
リポジトリ

tanstack-query について

tanstack-queryは、ReactアプリでTanStack Query v5を使うためのスキルです。設定、データ取得、キャッシュ、ミューテーション、デバッグに加え、移行時によくある問題の回避方法を扱います。

主な機能

  1. 01QueryClientとプロバイダーの設定
  2. 02クエリとミューテーションの実装
  3. 03キャッシュと無効化の管理
  4. 04楽観的更新の利用
  5. 05React Query DevToolsでのデバッグ
  6. 06v5移行時によくある問題の回避

活用シーン

  1. 01Reactアプリにサーバー状態の取得を追加
  2. 02キャッシュを更新するミューテーション処理の実装
  3. 03開発中のクエリのデバッグ
  4. 04TanStack Query v5の用法にコードを移行

インストール

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

$npx skills add https://github.com/jackspace/claudeskillz --skill tanstack-query

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

同梱ファイルREADME.mdSKILL.json

詳細

TanStack Query(React Query)v5

ステータス:本番環境で利用可能 ✅ 最終更新日:2025-10-22 依存関係:React 18.0+、TypeScript 4.7+(推奨) 最新バージョン:@tanstack/[email protected]、@tanstack/[email protected]


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

1. 依存関係をインストールする
npm install @tanstack/react-query@latest
npm install -D @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 です)
  • cacheTime ではなく gcTime を使用してください(v5 で名称が変更されました)
  • DevTools は Provider の内側に配置してください
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 })
  • isLoading ではなく isPending を使用してください(isLoading は現在、「保留中かつフェッチ中」を意味します)
  • 適切なエラー処理のため、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)
npm install @tanstack/react-query

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

# Optional: ESLint plugin for best practices
npm install -D @tanstack/eslint-plugin-query

各パッケージの役割:

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

バージョン要件:

  • React 18.0 以上(useSyncExternalStore を使用します)
  • より良い型推論には TypeScript 4.7+ が必要です(任意ですが推奨)
ステップ 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:データが「古い」とみなされ、再フェッチされる可能性が生じるまでの時間
    • 0(デフォルト):データはすぐに古くなり、マウント時やフォーカス時に再フェッチされます
    • 1000 * 60 * 5:データは 5 分間新鮮な状態に保たれ、その間は再フェッチされません
    • Infinity:データは古くならず、手動での無効化のみが行われます
  • 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:カスタムクエリフックを作成する

パターン:再利用可能なクエリフック

// src/api/todos.ts - API functions
export type Todo = {
  id: number
  title: string
  completed: boolean
}

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

export async function fetchTodoById(id: number): Promise<Todo> {
  const response = await fetch(`/api/todos/${id}`)
  if (!response.ok) {
    throw new Error(`Failed to fetch todo ${id}: ${response.statusText}`)
  }
  return response.json()
}

// src/hooks/useTodos.ts - Query hooks
import { useQuery, queryOptions } from '@tanstack/react-query'
import { fetchTodos, fetchTodoById } from '../api/todos'

// Query options factory (v5 pattern for reusability)
export const todosQueryOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 1000 * 60, // 1 minute
})

export function useTodos() {
  return useQuery(todosQueryOptions)
}

export function useTodo(id: number) {
  return useQuery({
    queryKey: ['todos', id],
    queryFn: () => fetchTodoById(id),
    enabled: !!id, // Only fetch if id is truthy
  })
}

queryOptions ファクトリーを使用する理由:

  • 型推論が適切に機能します
  • useQuery、useSuspenseQuery、prefetchQuery 間で再利用できます
  • どこでも一貫した queryKey と queryFn を使用できます
  • テストと保守が容易になります

クエリキーの構造:

  • ['todos'] - すべての todo の一覧
  • ['todos', id] - 単一の todo の詳細
  • ['todos', 'filters', { status: 'completed' }] - フィルターされた一覧
  • より具体的なキーは部分集合です(['todos'] を無効化すると、すべてのキーが無効化されます)
ステップ 5:楽観的更新でミューテーションを実装する
// src/hooks/useTodoMutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { Todo } from '../api/todos'

type AddTodoInput = {
  title: string
}

type UpdateTodoInput = {
  id: number
  completed: boolean
}

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

  return useMutation({
    mutationFn: async (newTodo: AddTodoInput) => {
      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()
    },

    // Optimistic update
    onMutate: async (newTodo) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['todos'] })

      // Snapshot previous value
      const previousTodos = queryClient.getQueryData<Todo[]>(['todos'])

      // Optimistically update
      queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [
        ...old,
        { id: Date.now(), ...newTodo, completed: false },
      ])

      // Return context with snapshot
      return { previousTodos }
    },

    // Rollback on error
    onError: (err, newTodo, context) => {
      queryClient.setQueryData(['todos'], context?.previousTodos)
    },

    // Always refetch after error or success
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })
}

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

  return useMutation({
    mutationFn: async ({ id, completed }: UpdateTodoInput) => {
      const response = await fetch(`/api/todos/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ completed }),
      })
      if (!response.ok) throw new Error('Failed to update todo')
      return response.json()
    },

    onSuccess: (updatedTodo) => {
      // Update the specific todo in cache
      queryClient.setQueryData<Todo>(['todos', updatedTodo.id], updatedTodo)

      // Invalidate list to refetch
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })
}

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

  return useMutation({
    mutationFn: async (id: number) => {
      const response = await fetch(`/api/todos/${id}`, { method: 'DELETE' })
      if (!response.ok) throw new Error('Failed to delete todo')
    },

    onSuccess: (_, deletedId) => {
      queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
        old.filter(todo => todo.id !== deletedId)
      )
    },
  })
}

楽観的更新のパターン:

  1. onMutate:クエリをキャンセルし、古いデータのスナップショットを取得して、キャッシュを楽観的に更新します
  2. onError:ミューテーションが失敗した場合、スナップショットの状態にロールバックします
  3. onSettled:キャッシュがサーバーと一致するように再取得します

使用する場面:

  • UX を向上させるために、UI ですぐにフィードバックを返したい場合
  • リスクの低いミューテーション(ToDo の切り替え、いいねボタン)
  • 重要なデータ(支払い、アカウント設定)には使用しないでください
ステップ 6:DevTools を設定する
// Already set up in main.tsx, but here are advanced options:

import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

<ReactQueryDevtools
  initialIsOpen={false}
  buttonPosition="bottom-right"
  position="bottom"

  // Custom toggle button
  toggleButtonProps={{
    style: { marginBottom: '4rem' },
  }}

  // Custom panel styles
  panelProps={{
    style: { height: '400px' },
  }}

  // Only show in dev (already tree-shaken in production)
  // But can add explicit check:
  // {import.meta.env.DEV && <ReactQueryDevtools />}
/>

DevTools の機能:

  • すべてのクエリとその状態を表示します
  • クエリキャッシュの内容を確認します
  • クエリを手動で再取得します
  • 実行中のミューテーションを表示します
  • クエリの依存関係を調べます
  • デバッグ用に状態をエクスポートします
ステップ 7:エラーバウンダリーを設定する
// src/components/ErrorBoundary.tsx
import { Component, type ReactNode } from 'react'
import { QueryErrorResetBoundary, useQueryErrorResetBoundary } from '@tanstack/react-query'

type Props = { children: ReactNode }
type State = { hasError: boolean }

class ErrorBoundaryClass extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError() {
    return { hasError: true }
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>Something went wrong</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      )
    }
    return this.props.children
  }
}

// Wrapper with TanStack Query error reset
export function ErrorBoundary({ children }: Props) {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundaryClass onReset={reset}>
          {children}
        </ErrorBoundaryClass>
      )}
    </QueryErrorResetBoundary>
  )
}

// Usage with throwOnError option:
function useTodosWithErrorBoundary() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    throwOnError: true, // Throw errors to error boundary
  })
}

// Or conditional:
function useTodosConditionalError() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    throwOnError: (error, query) => {
      // Only throw server errors, handle network errors locally
      return error instanceof Response && error.status >= 500
    },
  })
}

エラー処理の方法:

  • ローカル処理:クエリから isError と error を使用します
  • グローバル処理:throwOnError を指定したエラーバウンダリーを使用します
  • 併用:条件付きの throwOnError 関数を使用します
  • 集約処理:QueryCache のグローバルエラーハンドラーを使用します

重要なルール

必ず行うこと

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

// 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
絶対に行わないこと

❌ 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 件の問題を防ぎます。

問題 #1:オブジェクト構文が必須

エラー:useQuery is not a function または型エラー 出典:v5 Migration Guide 発生理由:v5 ではすべての関数オーバーロードが削除され、オブジェクト構文のみが機能します 予防策:常に useQuery({ queryKey, queryFn, ...options }) を使用します

変更前(v4):

useQuery(['todos'], fetchTodos, { staleTime: 5000 })

変更後(v5):

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 5000
})
問題 #2:クエリコールバックの削除

エラー:コールバックが実行されない、TypeScript エラー 出典:v5 Breaking Changes 発生理由:クエリから onSuccess、onError、onSettled が削除されました(ミューテーションでは引き続き機能します) 予防策:副作用には useEffect を使用するか、ロジックをミューテーションのコールバックに移動します

変更前(v4):

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  onSuccess: (data) => {
    console.log('Todos loaded:', data)
  },
})

変更後(v5):

const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    console.log('Todos loaded:', data)
  }
}, [data])
問題 #3:ステータスの loading → pending

エラー:UI の読み込み状態が正しく表示されない 出典:v5 Migration: isLoading renamed 発生理由:status: 'loading' が status: 'pending' に改称され、isLoading の意味も変更されました 予防策:初回読み込みには isPending を使用し、「pending かつ fetching」の状態には isLoading を使用します

変更前(v4):

const { data, isLoading } = useQuery(...)
if (isLoading) return <div>Loading...</div>

変更後(v5):

const { data, isPending, isLoading } = useQuery(...)
if (isPending) return <div>Loading...</div>
// isLoading = isPending && isFetching (fetching for first time)
問題 #4:cacheTime → gcTime

エラー:cacheTime is not a valid option 出典:v5 Migration: gcTime 発生理由:「ガベージコレクション時間」をより適切に表す名前に変更されました 予防策:cacheTime の代わりに gcTime を使用します

変更前(v4):

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  cacheTime: 1000 * 60 * 60,
})

変更後(v5):

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  gcTime: 1000 * 60 * 60,
})
Issue #5:useSuspenseQuery + enabled

エラー:型エラー。enabled オプションは使用できません ソース:GitHub ディスカッション #6206 発生する理由:Suspense はデータが利用可能であることを保証するため、条件付きで無効にできません 予防策:enabled オプションではなく、条件付きレンダリングを使用します

変更前(v4/誤り):

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

変更後(v5/正しい):

// Conditional rendering:
{id ? (
  <TodoComponent id={id} />
) : (
  <div>No ID selected</div>
)}

// Inside TodoComponent:
function TodoComponent({ id }: { id: number }) {
  const { data } = useSuspenseQuery({
    queryKey: ['todo', id],
    queryFn: () => fetchTodo(id),
    // No enabled option needed
  })
  return <div>{data.title}</div>
}
Issue #6:initialPageParam が必須

エラー:initialPageParam is required という型エラー ソース:v5 への移行:無限クエリ 発生する理由:v4 では最初の pageParam として undefined が渡されていましたが、v5 では明示的な値が必要です 予防策:無限クエリでは必ず initialPageParam を指定します

変更前(v4):

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam),
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

変更後(v5):

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // ✅ Required
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
Issue #7:keepPreviousData が削除

エラー:keepPreviousData is not a valid option ソース:v5 への移行:placeholderData 発生する理由:より柔軟な placeholderData 関数に置き換えられました 予防策:placeholderData: keepPreviousData ヘルパーを使用します

変更前(v4):

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

変更後(v5):

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

useQuery({
  queryKey: ['todos', page],
  queryFn: () => fetchTodos(page),
  placeholderData: keepPreviousData,
})
Issue #8:TypeScript のエラー型のデフォルト

エラー:エラー処理に関する型エラー ソース:v5 への移行:エラー型 発生する理由:v4 では unknown が使用されていましたが、v5 ではデフォルトで Error 型になります 予防策:Error 型以外をスローする場合は、エラー型を明示的に指定します

変更前(v4 - エラーは unknown):

const { error } = useQuery({
  queryKey: ['data'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw 'custom error string'
    return data
  },
})
// error: unknown

変更後(v5 - カスタムエラー型を指定):

const { error } = useQuery<DataType, string>({
  queryKey: ['data'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw 'custom error string'
    return data
  },
})
// error: string | null

// Or better: always throw Error objects
const { error } = useQuery({
  queryKey: ['data'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw new Error('custom error')
    return data
  },
})
// error: Error | null (default)

設定ファイルのリファレンス

package.json(完全な例)
{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "@tanstack/react-query": "^5.90.5"
  },
  "devDependencies": {
    "@tanstack/react-query-devtools": "^5.90.2",
    "@tanstack/eslint-plugin-query": "^5.90.2",
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.4",
    "typescript": "^5.6.3",
    "vite": "^6.0.1"
  }
}

これらのバージョンを使用する理由:

  • React 18.3.1 - useSyncExternalStore に必要です
  • TanStack Query 5.90.5 - v5 のすべての修正を含む最新の安定版です
  • DevTools 5.90.2 - query のバージョンに合わせています
  • TypeScript 5.6.3 - query の型を最適に推論します
tsconfig.json(TypeScript の設定)
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    /* TanStack Query specific */
    "esModuleInterop": true,
    "resolveJsonModule": true
  },
  "include": ["src"]
}
.eslintrc.cjs(TanStack Query プラグインを使用した ESLint の設定)
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
    'plugin:@tanstack/eslint-plugin-query/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parser: '@typescript-eslint/parser',
  plugins: ['react-refresh', '@tanstack/query'],
  rules: {
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
  },
}

ESLint プラグインで検出できる項目:

  • クエリキーをインラインではなく参照として使用している
  • queryFn がない
  • v5 で v4 のパターンを使用している
  • useEffect の依存関係が正しくない

よく使われるパターン

パターン 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 が変更されると、前のクエリは自動的にキャンセルされます
  • 適切にクリーンアップするため、fetch に signal を渡します
  • ブラウザーが保留中の fetch リクエストを中止します

同梱リソースの使用

テンプレート(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
参考資料(references/)

必要に応じて読み込む詳細ドキュメント:

  • v4-to-v5-migration.md - v4 → v5 の完全な移行ガイド
  • best-practices.md - リクエストのウォーターフォール、キャッシュ戦略、パフォーマンス
  • common-patterns.md - 再利用可能なクエリ、楽観的更新、無限スクロール
  • typescript-patterns.md - 型安全性、ジェネリクス、型推論
  • testing.md - MSW、React Testing Library を使ったテスト
  • top-errors.md - 解決方法付きの8件以上のエラーすべて

Claude がこれらを読み込むタイミング:

  • 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 を使います(テーマ、認証トークン)

依存関係

必須:

推奨:

任意:

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

公式ドキュメント


パッケージのバージョン(2025-10-22 に確認)

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

確認内容:

  • npm view @tanstack/react-query version → 5.90.5
  • npm view @tanstack/react-query-devtools version → 5.90.2
  • 最終確認日:2025-10-22

本番環境での例

このスキルは、以下の本番環境で使われるパターンに基づいています:

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

トラブルシューティング

問題:「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

コメント