tanstack-query について
React と TypeScript を使う TanStack Query v5 ユーザー向けの参照スキルです。クエリとミューテーション、キャッシュ、移行、SSR、オフライン対応、バージョン固有の問題を解説します。
主な機能
- 01クエリとミューテーションのパターン
- 02キャッシュ設定と無効化
- 03楽観的更新とエラー処理
- 04SSR、ページネーション、オフラインのガイド
- 05v4 から v5 への移行情報
活用シーン
- 01React アプリでのデータ取得実装
- 02プロジェクトの v4 から v5 への更新
- 03クエリのキャッシュと再取得の設定
- 04ハイドレーションやミューテーションの問題調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ marcioaltoe/ skills --skill tanstack-queryインストール後、Claude Code で/tanstack-queryを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlrules/cache-configuration.mdcache-invalidation.mderr-error-handling.mdinf-infinite-queries.mdmut-basics.mdmut-optimistic-updates.mdoffline-support.mdparallel-queries.mdperf-optimization.mdpf-prefetching.mdqk-query-keys.mdssr-hydration.md詳細
TanStack Query v5
バージョン: @tanstack/react-[email protected] 必要条件: React 18.0+、TypeScript 4.7+
v5 の新機能
- useMutationState — props のバケツリレーを使わずに、コンポーネント間でミューテーションを追跡します
- シンプルな楽観的更新 — 保留中のミューテーションの
variablesを使うため、キャッシュを操作する必要はありません - throwOnError —
useErrorBoundaryから名称が変更されました - networkMode — オフライン/PWA に対応します(
online|always|offlineFirst) - combine に対応した useQueries — 並列クエリの結果を 1 つのオブジェクトにまとめます
- infiniteQueryOptions — 無限クエリ向けの型安全なファクトリーです(
queryOptionsに対応) - maxPages — 無限クエリのキャッシュ内のページ数を制限します(双方向ページネーションが必要です)
- ミューテーションのコールバックシグネチャの変更(v5.89+) —
onError/onSuccess/onSettledが 4 つのパラメーターを受け取るようになりました(onMutateResultが追加されました)
クイックセットアップ
npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latest
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
refetchOnWindowFocus: false,
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
統合 Devtools(TanStack ライブラリを複数使う場合に推奨)
Query と Router(またはほかの TanStack ライブラリ)を使う場合は、個別の Devtools コンポーネントではなく、統合された TanStackDevtools シェルを使用してください。
npm install -D @tanstack/react-devtools
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<TanStackDevtools
config={{ position: 'bottom-right' }}
plugins={[
{ name: 'TanStack Query', render: <ReactQueryDevtoolsPanel /> },
// Add more plugins: Router, etc.
]}
/>
</QueryClientProvider>
)
}
TanStackDevtools 内に埋め込む場合は、*Panel バリアント(ReactQueryDevtoolsPanel、TanStackRouterDevtoolsPanel)を使用してください。
import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'
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()
},
})
function useTodos() {
return useQuery(todosQueryOptions)
}
function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newTodo: { title: string }) => {
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'] })
},
})
}
ルールのカテゴリ
| 優先度 | カテゴリ | ルールファイル | 影響 |
|---|---|---|---|
| CRITICAL | クエリキー | rules/qk-query-keys.md | キャッシュの不具合やデータの不整合を防ぎます |
| CRITICAL | キャッシュ | rules/cache-configuration.md | パフォーマンスとデータの鮮度を最適化します |
| HIGH | 無効化 | rules/cache-invalidation.md | 古くなったデータが適切に更新されるようにします |
| HIGH | ミューテーション | rules/mut-basics.md | 書き込み後のデータ整合性を保ちます |
| HIGH | 楽観的更新 | rules/mut-optimistic-updates.md | ミューテーション中も UI の応答性を保ちます |
| HIGH | エラー処理 | rules/err-error-handling.md | ユーザー体験の低下を防ぎます |
| MEDIUM | プリフェッチ | rules/pf-prefetching.md | 体感パフォーマンスを向上させます |
| MEDIUM | 無限クエリ | rules/inf-infinite-queries.md | ページネーションの不具合を防ぎます |
| MEDIUM | SSR/ハイドレーション | rules/ssr-hydration.md | 適切なサーバーレンダリングを可能にします |
| MEDIUM | 並列クエリ | rules/parallel-queries.md | 動的な並列データ取得を可能にします |
| LOW | パフォーマンス | rules/perf-optimization.md | 不要な再レンダリングを減らします |
| LOW | オフライン対応 | rules/offline-support.md | オフラインファーストのパターンを可能にします |
重要なルール
必ず行うこと
- すべてのフックでオブジェクト構文を使用する:
useQuery({ queryKey, queryFn, ...options }) - クエリキーには配列を使用する:
['todos']、['todos', id]、['todos', { filter }] - queryFn でエラーをスローする:
if (!res.ok) throw new Error('Failed') - 初回読み込みには isPending を使用する:
if (isPending) return <Loading /> - ミューテーション後に無効化する:
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }) - queryOptions ファクトリーを使用する:
useQuery、useSuspenseQuery、prefetchQuery間で再利用します - gcTime を使用する(cacheTime ではなく):v5 で名称が変更されました
絶対に行わないこと
- v4 の配列/関数構文:
useQuery(['todos'], fetchTodos)— v5 で削除されました - クエリのコールバック:クエリから
onSuccess/onError/onSettledが削除されました(ミューテーションでは引き続き使用できます)。代わりにuseEffectを使用してください - 「まだデータがない」状態の判定に isLoading を使うこと:v5 で意味が変わりました。
isPendingを使用してください - useSuspenseQuery で enabled を使うこと:使用できません。条件付きレンダリングを使用してください
- keepPreviousData:削除されました。
placeholderData: keepPreviousDataを使用してください - パラメーターの変更に refetch() を使うこと:代わりにパラメーターを queryKey に含めてください。クエリは自動的に再取得されます
v4→v5 移行チートシート
| v4 | v5 | 備考 |
|---|---|---|
useQuery(['key'], fn, opts) | useQuery({ queryKey, queryFn, ...opts }) | オブジェクト構文のみ |
cacheTime | gcTime | 名前を変更 |
isLoading (データなし) | isPending | isLoading = isPending && isFetching |
keepPreviousData: true | placeholderData: keepPreviousData | keepPreviousData ヘルパーをインポート |
useErrorBoundary | throwOnError | 名前を変更 |
クエリの onSuccess/onError/onSettled | 削除 | 副作用には useEffect を使用 |
pageParam = 0 のデフォルト値 | initialPageParam: 0 | 無限クエリでは必須 |
status: 'loading' | status: 'pending' | 名前を変更 |
onError(err, vars, ctx) | onError(err, vars, onMutateResult, ctx) | v5.89+ で4番目のパラメーターを追加 |
既知の問題(v5.90.x)
- ストリーミング SSR のハイドレーション不一致 —
void prefetchQueryとuseSuspenseQueryを併用し、条件付きでisFetchingに基づいてレンダリングすると、ハイドレーションエラーが発生します。回避策:prefetchをawaitするか、fetchStatusに基づくレンダリングを避けてください - プリフェッチ時の useQuery ハイドレーションエラー —
useQueryとサーバー側のプリフェッチを併用すると、サーバーとクライアント間でisLoadingが一致しない場合があります。代わりにuseSuspenseQueryを使用してください - エラー状態のクエリでは refetchOnMount が無視される — エラーは常に古い状態として扱われます。
refetchOnMount: falseに加えてretryOnMount: falseを使用してください - useMutationState の型 — あいまいな照合により、
mutation.state.variablesはunknown型になります。selectコールバック内で明示的にキャストしてください - invalidateQueries はアクティブなクエリのみを再取得する — 非アクティブなクエリも含めるには
refetchType: 'all'を使用してください - 読み取り専用のクエリキーが v5.90.8 で動作しない — v5.90.9+ で修正済み
主なパターン
// Dependent queries (B waits for A)
const { data: user } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) })
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchPosts(user!.id),
enabled: !!user,
})
// Parallel queries
const results = useQueries({
queries: ids.map(id => ({ queryKey: ['item', id], queryFn: () => fetchItem(id) })),
combine: (results) => ({ data: results.map(r => r.data), pending: results.some(r => r.isPending) }),
})
// Prefetch on hover
const handleHover = () => queryClient.prefetchQuery({ queryKey: ['item', id], queryFn: () => fetchItem(id) })
// Infinite scroll
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
// Query cancellation
queryFn: async ({ signal }) => {
const res = await fetch(`/api/search?q=${query}`, { signal })
return res.json()
}
// Data transformation
useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: (data) => data.filter(t => t.completed) })
コメント
Markdown 対応