mcpField
T
tanstack-query

tanstack-query

github.com/jezweb開発1.1k2.5k 回インストール3 週間前に更新
リポジトリ

tanstack-query について

React アプリ向けの TanStack Query v5 スキルガイドです。クエリとミューテーションのパターン、バージョン移行の要点、よくある実装ミスの回避策を扱います。

主な機能

  1. 01クエリとミューテーションのフックパターン
  2. 02楽観的更新とミューテーション追跡
  3. 03オフライン対応のネットワークモード
  4. 04並列クエリと無限クエリのガイド
  5. 05v5 移行時の問題と対処法

活用シーン

  1. 01React のデータ取得フロー構築
  2. 02コンポーネント間で処理中のミューテーションを追跡
  3. 03オフライン時にキャッシュ済みクエリを利用
  4. 04アプリを v4 から v5 に移行

インストール

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

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

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

同梱ファイル

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

README.md
.claude-plugin/
plugin.json
assets/
example-template.txt
references/
best-practices.md
common-patterns.md
example-reference.md
testing.md
top-errors.md
typescript-patterns.md
v4-to-v5-migration.md
rules/
tanstack-query.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
use-mutation-basic.tsx
use-mutation-optimistic.tsx
use-query-basic.tsx

詳細

TanStack Query(React Query)v5

最終更新日:2026-01-20 バージョン:@tanstack/[email protected]、@tanstack/[email protected] 必要条件:React 18.0+(useSyncExternalStore)、TypeScript 4.7+(推奨)


v5 の新機能

useMutationState - コンポーネント間でのミューテーション追跡

props のバケツリレーなしで、どこからでもミューテーションの状態にアクセスできます。

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

function GlobalLoadingIndicator() {
  // Get all pending mutations
  const pendingMutations = useMutationState({
    filters: { status: 'pending' },
    select: (mutation) => mutation.state.variables,
  })

  if (pendingMutations.length === 0) return null
  return <div>Saving {pendingMutations.length} items...</div>
}

// Filter by mutation key
const todoMutations = useMutationState({
  filters: { mutationKey: ['addTodo'] },
})
シンプルになった楽観的更新

variables を使う新しいパターンです。キャッシュの操作もロールバックも必要ありません。

function TodoList() {
  const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

  const addTodo = useMutation({
    mutationKey: ['addTodo'],
    mutationFn: (newTodo) => api.addTodo(newTodo),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  // Show optimistic UI using variables from pending mutations
  const pendingTodos = useMutationState({
    filters: { mutationKey: ['addTodo'], status: 'pending' },
    select: (mutation) => mutation.state.variables,
  })

  return (
    <ul>
      {todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
      {/* Show pending items with visual indicator */}
      {pendingTodos.map((todo, i) => (
        <li key={`pending-${i}`} style={{ opacity: 0.5 }}>{todo.title}</li>
      ))}
    </ul>
  )
}
throwOnError - エラーバウンダリー

useErrorBoundary から名称が変更されました(破壊的変更)。

import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
          <div>
            Error! <button onClick={resetErrorBoundary}>Retry</button>
          </div>
        )}>
          <Todos />
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  )
}

function Todos() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    throwOnError: true, // ✅ v5 (was useErrorBoundary in v4)
  })
  return <div>{data.map(...)}</div>
}
ネットワークモード(オフライン/PWA サポート)

オフライン時の動作を制御します。

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      networkMode: 'offlineFirst', // Use cache when offline
    },
  },
})

// Per-query override
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  networkMode: 'always', // Always try, even offline (for local APIs)
})
モード動作
online(デフォルト)オンライン時のみフェッチします
always常に試行します(ローカル API/サービスワーカー API に便利です)
offlineFirstまずキャッシュを使用し、オンライン時にフェッチします

一時停止状態の検出:

const { isPending, fetchStatus } = useQuery(...)
// isPending + fetchStatus === 'paused' = offline, waiting for network
combine を使った useQueries

並列クエリの結果をまとめます。

const results = useQueries({
  queries: userIds.map(id => ({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id),
  })),
  combine: (results) => ({
    data: results.map(r => r.data),
    pending: results.some(r => r.isPending),
    error: results.find(r => r.error)?.error,
  }),
})

// Access combined result
if (results.pending) return <Loading />
console.log(results.data) // [user1, user2, user3]
infiniteQueryOptions ヘルパー

無限クエリ用の型安全なファクトリーです(queryOptions に対応します)。

import { infiniteQueryOptions, useInfiniteQuery, prefetchInfiniteQuery } from '@tanstack/react-query'

const todosInfiniteOptions = infiniteQueryOptions({
  queryKey: ['todos', 'infinite'],
  queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

// Reuse across hooks
useInfiniteQuery(todosInfiniteOptions)
useSuspenseInfiniteQuery(todosInfiniteOptions)
prefetchInfiniteQuery(queryClient, todosInfiniteOptions)
maxPages - メモリの最適化

無限クエリでキャッシュに保存するページ数を制限します。

useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) => fetchPosts(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor, // Required with maxPages
  maxPages: 3, // Only keep 3 pages in memory
})

注: maxPages を使用するには、双方向ページネーション(getNextPageParam と getPreviousPageParam の両方)が必要です。


クイックセットアップ

npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latest
ステップ 2:プロバイダーと設定
// src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 min
      gcTime: 1000 * 60 * 60, // 1 hour (v5: renamed from cacheTime)
      refetchOnWindowFocus: false,
    },
  },
})

<QueryClientProvider client={queryClient}>
  <App />
  <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
ステップ 3:クエリとミューテーションのフック
// src/hooks/useTodos.ts
import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'

// Query options factory (v5 pattern)
export const todosQueryOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: async () => {
    const res = await fetch('/api/todos')
    if (!res.ok) throw new Error('Failed to fetch')
    return res.json()
  },
})

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

export function useAddTodo() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: async (newTodo) => {
      const res = await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newTodo),
      })
      if (!res.ok) throw new Error('Failed to add')
      return res.json()
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })
}

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

  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 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)

✅ cacheTime ではなく gcTime を使用する

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} />}

❌ エラーになったクエリで refetchOnMount: false に頼らない

// Doesn't work - errors are always stale
useQuery({
  queryKey: ['data'],
  queryFn: failingFetch,
  refetchOnMount: false,  // ❌ Ignored when query has error
})

// Use retryOnMount instead
useQuery({
  queryKey: ['data'],
  queryFn: failingFetch,
  refetchOnMount: false,
  retryOnMount: false,  // ✅ Prevents refetch for errored queries
  retry: 0,
})

既知の問題の防止

このスキルは、v5 への移行、SSR/ハイドレーションのバグ、よくあるミスに関する16 件の文書化された問題を防ぎます。

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

エラー:useQuery is not a function または型エラー 情報源:v5 移行ガイド 発生理由:v5 ではすべての関数オーバーロードが削除され、オブジェクト構文のみが動作します 防止策:常に useQuery({ queryKey, queryFn, ...options }) を使用する

変更前(v4):

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

変更後(v5):

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

エラー:コールバックが実行されない、TypeScript エラー 情報源:v5 の破壊的変更 発生理由:クエリから 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 の移行:isLoading の名称変更 発生理由: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)
Issue #4: cacheTime → gcTime

エラー: cacheTime is not a valid option 情報源: v5 への移行: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)
Issue #9: ストリーミングサーバーコンポーネントのハイドレーションエラー

エラー: Hydration failed because the initial UI does not match what was rendered on the server 情報源: GitHub Issue #9642 影響範囲: ストリーミング SSR(void プリフェッチパターン)を使用する v5.82.0 以降 発生理由: hydrate() は同期的に解決される一方、query.fetch() は非同期の retryer を作成するため、サーバーとクライアント間で isFetching/isStale に不一致が生じる競合状態です 予防策: useSuspenseQuery とストリーミングプリフェッチを使用する場合は fetchStatus に基づく条件付きレンダリングを行わないか、void パターンではなくプリフェッチを await してください

変更前(ハイドレーションエラーが発生):

// Server: void prefetch
streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });

// Client: conditional render on fetchStatus
const { data, isFetching } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <>{data && <div>{data}</div>} {isFetching && <Loading />}</>;

変更後(回避策):

// Option 1: Await prefetch
await streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });

// Option 2: Don't render based on fetchStatus with Suspense
const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <div>{data}</div>;  // No conditional on isFetching

状況: 既知の問題であり、メンテナーが調査中です。useSyncExternalStore への getServerSnapshot の実装が必要です。

Issue #10: プリフェッチ時の useQuery ハイドレーションエラー

エラー: ハイドレーション中にテキストコンテンツが一致しません 情報源: GitHub Issue #9399 影響範囲: サーバーサイドプリフェッチを使用する v5.x 発生理由: tryResolveSync が RSC ペイロード内の解決済み Promise を検出し、ハイドレーション中にデータを同期的に抽出することで、通常の pending 状態を迂回します 予防策: SSR では useQuery の代わりに useSuspenseQuery を使用するか、isLoading に基づく条件付きレンダリングを避けてください

変更前(ハイドレーションエラーが発生):

// Server Component
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos });

// Client Component
function Todos() {
  const { data, isLoading } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
  if (isLoading) return <div>Loading...</div>;  // Server renders this
  return <div>{data.length} todos</div>;  // Client hydrates with this
}

変更後(回避策):

// Use useSuspenseQuery instead
function Todos() {
  const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos });
  return <div>{data.length} todos</div>;
}

状況: 「修正すべき OSS の課題リストの最上位にあります」—メンテナー Ephem(2025 年 11 月)。useSyncExternalStore への getServerSnapshot の実装が必要です。

Issue #11: エラーになったクエリで refetchOnMount が反映されない

エラー: refetchOnMount: false にもかかわらず、マウント時にクエリが再取得される ソース: GitHub Issue #10018 影響するバージョン: v5.90.16+ 発生する理由: データのないエラー状態のクエリは、常に古いものとして扱われます。エラー状態が永続的に表示されるのを防ぐための意図的な動作です。 回避方法: refetchOnMount: false の代わりに(または併せて)retryOnMount: false を使用してください。

変更前(設定しても再取得される):

const { data, error } = useQuery({
  queryKey: ['data'],
  queryFn: () => { throw new Error('Fails') },
  refetchOnMount: false,  // Ignored when query is in error state
  retry: 0,
});
// Query refetches every time component mounts

変更後(正しい設定):

const { data, error } = useQuery({
  queryKey: ['data'],
  queryFn: failingFetch,
  refetchOnMount: false,
  retryOnMount: false,  // ✅ Prevents refetch on mount for errored queries
  retry: 0,
});

ステータス: 文書化された動作(意図的)。retryOnMount という名前はやや誤解を招きます。これは自動再試行ではなく、エラーになったクエリがマウント時に新しいフェッチを開始するかどうかを制御します。

Issue #12: Mutation コールバックのシグネチャに関する破壊的変更(v5.89.0)

エラー: mutation コールバックで TypeScript エラーが発生する ソース: GitHub Issue #9660 影響するバージョン: v5.89.0+ 発生する理由: variables と context の間に onMutateResult パラメーターが追加され、コールバックのシグネチャが 3 個のパラメーターから 4 個に変更されました。 回避方法: すべての mutation コールバックを、3 個ではなく 4 個のパラメーターを受け取るように更新してください。

変更前(v5.88 以前):

useMutation({
  mutationFn: addTodo,
  onError: (error, variables, context) => {
    // context is now onMutateResult, missing final context param
  },
  onSuccess: (data, variables, context) => {
    // Same issue
  }
});

変更後(v5.89.0+):

useMutation({
  mutationFn: addTodo,
  onError: (error, variables, onMutateResult, context) => {
    // onMutateResult = return value from onMutate
    // context = mutation function context
  },
  onSuccess: (data, variables, onMutateResult, context) => {
    // Correct signature with 4 parameters
  }
});

注: onMutate を使用しない場合、onMutateResult パラメーターは undefined になります。この破壊的変更はパッチバージョンで導入されました。

Issue #13: 読み取り専用クエリキーで部分一致が機能しない(v5.90.8)

エラー: Type 'readonly ["todos", string]' is not assignable to type '["todos", string]' ソース: GitHub Issue #9871 | PR #9872 で修正済み 影響するバージョン: v5.90.8 のみ(v5.90.9 で修正済み) 発生する理由: 部分的なクエリ一致により、読み取り専用クエリキー(as const を使用)の TypeScript 型が正しく機能しなくなりました。 回避方法: v5.90.9+ にアップグレードするか、v5.90.8 を使用し続ける必要がある場合は型アサーションを使用してください。

変更前(v5.90.8 - TypeScript エラー):

export function todoQueryKey(id?: string) {
  return id ? ['todos', id] as const : ['todos'] as const;
}
// Type: readonly ['todos', string] | readonly ['todos']

useMutation({
  mutationFn: addTodo,
  onSuccess: () => {
    queryClient.invalidateQueries({
      queryKey: todoQueryKey('123')
      // Error: readonly ['todos', string] not assignable to ['todos', string]
    });
  }
});

変更後(v5.90.9+):

// Works correctly with readonly types
queryClient.invalidateQueries({
  queryKey: todoQueryKey('123')  // ✅ No type error
});

ステータス: v5.90.9 で修正済み。openapi-react-query のように読み取り専用クエリキーを生成するコードジェネレーターのユーザーに、特に影響しました。

Issue #14: useMutationState で型推論が失われる

エラー: mutation.state.variables の型が実際の型ではなく unknown になる ソース: GitHub Issue #9825 影響するバージョン: すべての v5.x バージョン 発生する理由: あいまいな mutation キーの一致により、型推論が保証されません(queryClient.getQueryCache().find() と同じ問題です)。 回避方法: select コールバック内で明示的に型キャストしてください。

変更前(型推論が機能しない):

const addTodo = useMutation({
  mutationKey: ['addTodo'],
  mutationFn: (todo: Todo) => api.addTodo(todo),
});

const pendingTodos = useMutationState({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => {
    return mutation.state.variables;  // Type: unknown
  },
});

変更後(明示的な型キャストあり):

const pendingTodos = useMutationState({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => mutation.state.variables as Todo,
});
// Or cast the entire state:
select: (mutation) => mutation.state as MutationState<Todo, Error, Todo, unknown>

ステータス: あいまいな一致に関する既知の制限事項です。修正の予定はありません。

Issue #15: fetchQuery を StrictMode で使用した場合のクエリキャンセル

エラー: useQuery と fetchQuery() の使用時に CancelledError が発生する ソース: GitHub Issue #9798 影響する環境: 開発環境のみ(React StrictMode) 発生する理由: StrictMode ではマウントとアンマウントが 2 回行われます。useQuery がアンマウントされ、最後のオブザーバーになると、fetchQuery() も実行中であってもクエリがキャンセルされます。 回避方法: これは開発環境のみで発生する想定内の動作です。本番環境には影響しません。

例:

async function loadData() {
  try {
    const data = await queryClient.fetchQuery({
      queryKey: ['data'],
      queryFn: fetchData,
    });
    console.log('Loaded:', data);  // Never logs in StrictMode
  } catch (error) {
    console.error('Failed:', error);  // CancelledError
  }
}

function Component() {
  const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData });
  // In StrictMode, component unmounts/remounts, cancelling fetchQuery
}

回避策:

// Keep query observed with staleTime
const { data } = useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  staleTime: Infinity,  // Keeps query active
});

ステータス: StrictMode の想定内の動作であり、バグではありません。本番ビルドには影響しません。

Issue #16:invalidateQueries はアクティブなクエリのみを再取得する

エラー:invalidateQueries() を呼び出しても、非アクティブなクエリが再取得されない 出典:GitHub Issue #9531 影響範囲:v5.x のすべてのバージョン 発生する理由:ドキュメントの説明が誤解を招くものでした。デフォルトでは、invalidateQueries() は「すべて」ではなく「アクティブな」クエリのみを再取得します 予防策:refetchType: 'all' を使用して、非アクティブなクエリも強制的に再取得します

デフォルトの動作:

// Only active queries (currently being observed) will refetch
queryClient.invalidateQueries({ queryKey: ['todos'] });

非アクティブなクエリを再取得するには:

queryClient.invalidateQueries({
  queryKey: ['todos'],
  refetchType: 'all'  // Refetch active AND inactive
});

状況:「アクティブな」クエリを明確にするよう、ドキュメントは修正済みです。これは意図された動作です。


コミュニティのヒント

注:これらのヒントは、コミュニティのエキスパートやメンテナーのブログから引用しています。使用しているバージョンで確認してください。

ヒント:複数のリスナーがある場合のクエリオプション

出典:TkDodo's Blog - API Design Lessons | 信頼度:高 適用対象:v5.27.3 以降

複数のコンポーネントが異なるオプション(staleTime など)で同じクエリを使用する場合、今後のフェッチには「最後に書き込んだ値が優先される」ルールが適用されますが、現在進行中のクエリでは開始時のオプションが使われます。コンポーネントが異なるタイミングでマウントされると、予期しない動作につながることがあります。

予期しない動作の例:

// Component A mounts first
function ComponentA() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    staleTime: 5000,  // Applied initially
  });
}

// Component B mounts while A's query is in-flight
function ComponentB() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    staleTime: 60000,  // Won't affect current fetch, only future ones
  });
}

推奨される方法:

// Write options as functions that reference latest values
const getStaleTime = () => shouldUseLongCache ? 60000 : 5000;

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: getStaleTime(),  // Evaluated on each render
});
ヒント:refetch() はパラメーター変更用ではありません

出典:TanStack Query でよくある間違いを避ける | 信頼度:高

refetch() 関数は、同じパラメーターで再読み込みする場合(手動の「再読み込み」ボタンなど)にのみ使用してください。新しいパラメーター(フィルター、ページ番号、検索語など)を使う場合は、代わりにそれらをクエリキーに含めてください。

アンチパターン:

// ❌ Wrong - using refetch() for different parameters
const [page, setPage] = useState(1);
const { data, refetch } = useQuery({
  queryKey: ['todos'],  // Same key for all pages
  queryFn: () => fetchTodos(page),
});

// This refetches with OLD page value, not new one
<button onClick={() => { setPage(2); refetch(); }}>Next</button>

正しいパターン:

// ✅ Correct - include parameters in query key
const [page, setPage] = useState(1);
const { data } = useQuery({
  queryKey: ['todos', page],  // Key changes with page
  queryFn: () => fetchTodos(page),
  // Query automatically refetches when page changes
});

<button onClick={() => setPage(2)}>Next</button>  // Just update state

refetch() を使う場合:

// ✅ Manual refresh of same data (refresh button)
const { data, refetch } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});

<button onClick={() => refetch()}>Refresh</button>  // Same parameters

主なパターン

依存クエリ(クエリ B がクエリ A を待機):

const { data: posts } = useQuery({
  queryKey: ['users', userId, 'posts'],
  queryFn: () => fetchUserPosts(userId),
  enabled: !!user, // Wait for user
})

並列クエリ(複数を同時にフェッチ):

const results = useQueries({
  queries: ids.map(id => ({ queryKey: ['todos', id], queryFn: () => fetchTodo(id) })),
})

プリフェッチ(ホバー時に事前読み込み):

queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })

無限スクロール(useInfiniteQuery):

useInfiniteQuery({
  queryKey: ['todos', 'infinite'],
  queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
  initialPageParam: 0, // Required in v5
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

クエリのキャンセル(queryKey の変更時に自動キャンセル):

queryFn: async ({ signal }) => {
  const res = await fetch(`/api/todos?q=${search}`, { signal })
  return res.json()
}

データ変換(select):

select: (data) => data.filter(todo => todo.completed)

リクエストのウォーターフォールを避ける:可能な場合は並列でフェッチします(本当に依存関係がある場合を除き、クエリを連鎖させないでください)


公式ドキュメント:https://tanstack.com/query/latest | v5 への移行:https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5 | GitHub:https://github.com/TanStack/query | Context7:/websites/tanstack_query

コメント