tanstack-query について
React 18+ と TypeScript のプロジェクトで TanStack Query v5 を使うための開発スキルです。クエリ、ミューテーション、キャッシュ設定と移行時の注意点を扱います。
主な機能
- 01クエリとミューテーションの管理
- 02キャッシュと再取得の設定
- 03プリフェッチと無限クエリの利用
- 04DevTools とエラー処理の解説
- 05v4 から v5 への移行差分の説明
活用シーン
- 01React アプリへのサーバーデータ取得の導入
- 02ミューテーション後の関連クエリ更新
- 03ページングリストとデータのプリフェッチ
- 04v5 の状態フラグやオプションの問題調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ secondsky/ claude-skills --skill tanstack-queryインストール後、Claude Code で/tanstack-queryを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/example-template.txtexamples/basic-graphql-request.tsxbasic.tsxdefault-query-function.tsxinfinite-scroll.tsxnextjs-app-router.tsxoptimistic-update.tsxpagination.tsxprefetching.tsxreact-native.tsxREADME.mdsuspense.tsxreferences/advanced-setup.mdbest-practices.mdcommon-patterns.mdconfiguration-files.mdexample-reference.mdofficial-guides-map.mdtesting.mdtop-errors.mdtypescript-patterns.mdv4-to-v5-migration.mdscripts/example-script.shtemplates/custom-hooks-pattern.tsxdevtools-setup.tsxerror-boundary.tsxpackage.jsonprovider-setup.tsxquery-client-config.tsuse-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: truenetworkMode: 'online'(オフライン中はリクエストが一時停止します)。キャンセルを避けたい SSR/プリフェッチでは'always'に切り替えてください。 citeturn1search0turn1search1
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 クライアント + selectoptimistic-update.tsx- onMutate によるスナップショット/ロールバックpagination.tsx- placeholderData を使ったページ分割リストinfinite-scroll.tsx- useInfiniteQuery + IntersectionObserverprefetching.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/[email protected]- コアライブラリ[email protected]+- useSyncExternalStore フックを使用[email protected]+- React DOM レンダラー
推奨:
@tanstack/[email protected]- ビジュアルデバッガー(開発時のみ)@tanstack/[email protected]- ベストプラクティスのための ESLint ルール[email protected]+- 型安全性と型推論のため
任意:
@tanstack/query-sync-storage-persister- キャッシュを localStorage に永続化@tanstack/query-async-storage-persister- AsyncStorage に永続化(React Native)
公式ドキュメント
- TanStack Query ドキュメント:https://tanstack.com/query/latest
- React インテグレーション:https://tanstack.com/query/latest/docs/framework/react/overview
- v5 移行ガイド:https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5
- API リファレンス:https://tanstack.com/query/latest/docs/framework/react/reference/useQuery
- Context7 ライブラリ ID:
/websites/tanstack_query - GitHub リポジトリ:https://github.com/TanStack/query
- Discord コミュニティ:https://tlinz.com/discord
パッケージバージョン(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.4npm view @tanstack/react-query-devtools version→ 5.101.4npm 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 の型が正しく動作している
- 本番用ビルドが成功する
質問や問題がありますか?
- エラーの解決方法については
references/top-errors.mdを確認してください - セットアップ手順のすべてのステップを確認してください
- 公式ドキュメントを確認してください:https://tanstack.com/query/latest
- v5 構文(オブジェクト構文、gcTime、isPending)を使用していることを確認してください
- Discord に参加してください:https://tlinz.com/discord