tanstack-query-expert について
TanStack Query による非同期状態管理の設計やトラブルシューティングを支援する、React と Next.js 開発者向けのスキルです。キャッシュ、ミューテーション、楽観的更新に加え、Next.js App Router のサーバー側プリフェッチとハイドレーションを扱います。
主な機能
- 01型安全なクエリキーの設計
- 02キャッシュとリトライ動作の設定
- 03クエリとミューテーションのフック化
- 04無効化と楽観的更新の管理
- 05サーバー側プリフェッチとハイドレーション
活用シーン
- 01useEffect のデータ取得を TanStack Query に移行
- 02サーバー更新後のキャッシュ無効化の設計
- 03Next.js App Router でのデータのプリフェッチとハイドレーション
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill tanstack-query-expertインストール後、Claude Code で/tanstack-query-expertを入力して読み込まれたか確認します。
詳細
TanStack Query エキスパート
本番環境に対応した TanStack Query(旧 React Query)のエキスパートです。React や Next.js アプリケーションで、堅牢で高パフォーマンスな非同期状態管理レイヤーを構築できるよう開発者を支援します。宣言的なデータ取得、キャッシュの無効化、楽観的 UI 更新、バックグラウンド同期、エラーバウンダリ、サーバーサイドレンダリング(SSR)のハイドレーションパターンを熟知しています。
このスキルを使う場面
- データ取得ロジックの設定やリファクタリング(
useEffect+useStateの置き換え) - クエリキーの設計(配列ベースの厳密に型付けされたキー)
- グローバルまたはクエリ単位の
staleTime、gcTime、retryの動作設定 - POST/PUT/DELETE リクエスト用の
useMutationフックの作成 - ミューテーション後のキャッシュ無効化(
queryClient.invalidateQueries) - すぐに UX に反映する楽観的更新の実装
- TanStack Query と Next.js App Router の統合(Server Components + Client Boundary のハイドレーション)
基本概念
例では v5 のオブジェクト API を使用します。レスポンスが異なる場合はテナント/ユーザーごとにキーをスコープし、アカウント変更時にはプライベートキャッシュをクリアしてください。また、シークレットや別のユーザーのデータをデハイドレーションしないでください。以下の楽観的スナップショットは単一のミューテーションを想定しています。並行するミューテーションでは、ロールバックによって別の結果が消えないよう調整が必要です。
TanStack Query を使う理由
TanStack Query は単なるデータ取得用のライブラリではなく、非同期状態マネージャーです。キャッシュ、バックグラウンド更新、同じデータに対する複数リクエストの重複排除、ページネーション、標準搭載のローディング状態とエラー状態を処理します。
目安: スタックで TanStack Query が利用できる場合は、データ取得に useEffect を使わないでください。
クエリ定義のパターン
カスタムフックパターン(ベストプラクティス)
取得ロジック、TypeScript の型、クエリキーをまとめるため、useQuery の呼び出しは必ずカスタムフックに抽象化してください。
import { useQuery } from '@tanstack/react-query';
// 1. Define strict types
type User = { id: string; name: string; status: 'active' | 'inactive' };
// 2. Define the fetcher function
const fetchUser = async (userId: string): Promise<User> => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
};
// 3. Export a custom hook
export const useUser = (userId: string) => {
return useQuery({
queryKey: ['users', userId], // Array-based query key
queryFn: () => fetchUser(userId),
staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes (no background refetching)
enabled: !!userId, // Dependent query: only run if userId exists
});
};
高度なクエリキー
クエリキーはキャッシュを一意に識別します。配列である必要があり、順序も重要です。
// Filtering / Sorting
useQuery({
queryKey: ['issues', { status: 'open', sort: 'desc' }],
queryFn: () => fetchIssues({ status: 'open', sort: 'desc' })
});
// Factory pattern for query keys (Highly recommended for large apps)
export const issueKeys = {
all: ['issues'] as const,
lists: () => [...issueKeys.all, 'list'] as const,
list: (filters: string) => [...issueKeys.lists(), { filters }] as const,
details: () => [...issueKeys.all, 'detail'] as const,
detail: (id: number) => [...issueKeys.details(), id] as const,
};
ミューテーションとキャッシュの無効化
基本的なミューテーションと無効化
サーバー上のデータを変更した場合、古いデータが古くなったことをクライアントのキャッシュに必ず伝えてください。
import { useMutation, useQueryClient } from '@tanstack/react-query';
export const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newPost: { title: string }) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
if (!res.ok) throw new Error("Create post failed");
return res.json();
},
// On success, invalidate the 'posts' cache to trigger a background refetch
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
};
楽観的更新
サーバーからの応答前にキャッシュを更新し、リクエストが失敗した場合は元に戻すことで、ユーザーにすぐフィードバックを返します。
export const useUpdateTodo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateTodoFn,
// 1. Triggered immediately when mutate() is called
onMutate: async (newTodo) => {
// Cancel any outgoing refetches so they don't overwrite our optimistic update
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot the previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update to the new value
queryClient.setQueryData(['todos'], (old: any) =>
(old ?? []).map((todo: any) => todo.id === newTodo.id ? { ...todo, ...newTodo } : todo)
);
// Return a context object with the snapshotted value
return { previousTodos };
},
// 2. If the mutation fails, use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
// 3. Always refetch after error or success to ensure server sync
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
};
Next.js App Router との統合
Provider の初期化
// app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export default function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false, // Prevents aggressive refetching on tab switch
},
},
})
)
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
Server Component での事前取得(ハイドレーション)
サーバーでデータを事前取得し、props のバケツリレーや initialData を使わずにクライアントへ渡します。
// app/posts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import PostsList from './PostsList'; // Client Component
export default async function PostsPage() {
const queryClient = new QueryClient();
// Prefetch the data on the server
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPostsServerSide,
});
// Dehydrate the cache and pass it to the HydrationBoundary
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostsList />
</HydrationBoundary>
);
}
// app/posts/PostsList.tsx (Client Component)
'use client'
import { useQuery } from '@tanstack/react-query';
export default function PostsList() {
// Hydration supplies initial cache data; stale data may refetch on mount.
// It reads instantly from the dehydrated server cache.
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPostsClientSide,
});
return <div>{data?.map(post => <p key={post.id}>{post.title}</p>)}</div>;
}
ベストプラクティス
- ✅ 推奨: クエリキーファクトリーを作成し、ファイルごとに
['users']と['user']を書き間違えないようにします。 - ✅ 推奨: データが毎秒変わるわけではない場合は、グローバルな
staleTime(例:1000 * 60)を設定します。デフォルトのstaleTimeは0です。つまり、デフォルトではコンポーネントが再マウントされるたびに、TanStack Query がバックグラウンドで再取得します。 - ✅ 推奨:
queryClient.setQueryDataの使用は控えめにします。通常はinvalidateQueriesを実行し、TanStack Query に最新データを自然に再取得させるほうが適切です。 - ✅ 推奨: すべての
useMutationとuseQueryの呼び出しをカスタムフックに抽象化します。ビュー側ではconst { mutate } = useCreatePost()のように呼び出すだけにします。 - ✅ インラインのクエリ関数もサポートされています。変更される取得依存関係はすべてクエリキーに含めてください。関数がインラインだからという理由だけでメモ化する必要はありません。
- ❌ 非推奨: クエリデータをローカルの React 状態に同期しないでください(例:
useEffect(() => setLocalState(data), [data]))。クエリデータを直接使います。派生状態が必要な場合は、レンダリング時に導出してください。
トラブルシューティング
問題: ネットワークタブでフェッチが無限に繰り返されます。
解決策: queryFn を確認してください。fetch のロジックが正しく構成されていない場合や、return に到達する前に未処理の例外が発生する場合、TanStack Query はデフォルトで最大3回まで自動的に再試行します。不安定な useEffect でラップされている場合は、無限ループになります。デバッグ時は retry: false を確認してください。
問題: staleTime と gcTime(以前の cacheTime)の違いがわかりません。
解決策: staleTime はバックグラウンドで再フェッチが実行されるタイミングを制御します。gcTime は、コンポーネントのアンマウント後、非アクティブなデータがメモリに保持される時間を制御します。gcTime が staleTime より短い場合、データは stale になる前に削除されてしまいます!
制限事項
- タスクが上記の説明された範囲に明確に一致する場合にのみ、このスキルを使用してください。
- 出力を、環境固有の検証、テスト、専門家によるレビューの代わりとして扱わないでください。
- 必要な入力、権限、安全上の境界、成功基準が不足している場合は、作業を中断して確認を求めてください。