mcpField
A
accelint-tanstack-query-best-practices

accelint-tanstack-query-best-practices

github.com/gohypergiant開発25277 回インストール2 週間前に更新
リポジトリ

accelint-tanstack-query-best-practices について

accelint-tanstack-query-best-practicesは、Next.js App RouterやServer Componentsを含む、TanStack Queryを使ったReactアプリ開発者向けのスキルです。クエリ設定、キャッシュ設計、ミューテーション、パフォーマンス診断を場面別に案内します。

主な機能

  1. 01QueryClient設定のガイド
  2. 02安定したクエリキーのパターン
  3. 03キャッシュと無効化の戦略
  4. 04楽観的・悲観的ミューテーションのパターン
  5. 05オブザーバーのパフォーマンス診断

活用シーン

  1. 01サーバーとクライアントのTanStack Query設定
  2. 02クエリキーとキャッシュライフサイクルの設計
  3. 03操作に応じたミューテーション方式の選択
  4. 04過剰なクエリオブザーバーの診断

インストール

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

$npx skills add https://github.com/gohypergiant/agent-skills --skill accelint-tanstack-query-best-practices

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

同梱ファイル

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

assets/
output-report-template.md
query-client.ts
references/
caching-strategy.md
fundamentals.md
mutations-and-updates.md
patterns-and-pitfalls.md
query-client-setup.md
query-keys.md
server-integration.md

詳細

TanStack Query のベストプラクティス

Next.js App Router と Server Components を使用する最新の React アプリケーションにおける、TanStack Query のエキスパート向けパターン。

TanStack Query で絶対にしてはいけないこと

  • サーバーでシングルトンの QueryClient を絶対に使用しないでください - ユーザー間でデータ漏えいや競合状態が発生します。あるユーザーのキャッシュデータが別のユーザーに表示されないよう、リクエストごとに独立した QueryClient インスタンスを用意する必要があります。
  • クエリデータを絶対に useState に同期しないでください - バックグラウンドでの再取得、無効化、楽観的更新はすべてキャッシュを変更します。ローカル状態のコピーはすぐに古くなり、「保存したのに反映されない」という問題の原因になります。クエリデータを直接使用するか、useMemo で導出してください。
  • リスト項目のコンポーネント内にクエリを絶対に配置しないでください - N 個の項目に対して N 個のオブザーバーが作成され、キャッシュ更新のたびに O(n) の反復処理が発生します。200 個のリスト項目で useQuery を呼び出すと、200 件のネットワークリクエストと 200 個のオブザーバーが作成されます。クエリは親コンポーネントにまとめてください。
  • 不安定なクエリキーを絶対に使用しないでください - 順序が保証されない配列、Date.now() を使った時間依存のクエリ、または決定論的にシリアライズされないオブジェクトキーは、無限にキャッシュエントリを作成します。キーは安定していて決定論的でなければなりません。
  • 依存クエリで enabled ガードを絶対に省略しないでください - パラメーターが undefined のままクエリを実行すると、['tracks', undefined] のような不要なキャッシュエントリが作成され、実際のデータが届く前にネットワークリクエストを浪費します。
  • AbortController のシグナルを絶対に無視しないでください - クエリのキャンセルに対応しないと、アンマウントされたコンポーネントの実行中リクエストが残り、帯域幅を浪費するうえ、古いキャッシュエントリを更新するおそれがあります。
  • 重要度の高い処理や外部から発生するミューテーションに、絶対に楽観的更新を使用しないでください - 生命に関わる操作、監査証跡システム、外部イベントによって発生するミューテーションでは、UI がサーバーの状態と一致するよう、悲観的更新が必要です。
  • 構造的共有は無料だと絶対に考えないでください - 頻繁に更新される 1000 件を超えるデータセットでは、構造的共有に伴う O(n) の深い等価性チェックが CPU 負荷になります。大規模で頻繁に変化するデータでは、structuralSharing: false を指定して無効にしてください。
  • 楽観的更新で onSettled を絶対に省略しないでください - onError が例外をスローした場合でも、onSettled はクリーンアップを保証します。これがないと、エラーハンドラーが失敗した際に UI が破損した状態のままになることがあります。リソースのクリーンアップとキャッシュの整合性を保つため、必ず onMutate と onSettled を組み合わせてください。
  • キャッシュの無効化は同期的に行われると絶対に考えないでください - invalidateQueries はバックグラウンドで再取得を開始するため、楽観的更新と競合することがあります。ミューテーションが完了する前にバックグラウンドの再取得が楽観的変更を上書きしないよう、onMutate 内で cancelQueries を使用してください。
  • 構造的な比較を行わずに setQueryData を絶対に使用しないでください - キャッシュデータを直接設定すると、構造的共有が回避され、参照等価性による最適化が損なわれます。変更されていない部分の参照を維持するため、setQueryData(key, newValue) ではなく、アップデーター関数でラップしてください:setQueryData(key, (old) => ({ ...old, changed: value }))。
  • ハイドレーションの不一致への対処を絶対に忘れないでください - サーバーでレンダリングされたデータがクライアントの想定と異なる場合があります(タイムスタンプ、ユーザー固有のデータ、ランダムなコンテンツなど)。コンテナーに suppressHydrationWarning を使用するか、安定したタイムスタンプと一貫したデータソースを使って、サーバーとクライアントで決定論的にレンダリングされるようにしてください。

TanStack Query を使用する前に確認すること

状態の分類
  • これはサーバー状態ですか、それともクライアント状態ですか? TanStack Query はサーバー状態(API データ、データベースレコード、外部システムの状態)を管理します。UI 状態(モーダル、テーマ、フォームの下書き)は Zustand または useState で管理します。
  • このデータは初回レンダリング後に変化しますか? 静的な参照データには、TanStack Query の再取得機能が不要な場合があります。より簡単な代替手段で十分かどうか検討してください。
キャッシュ戦略
  • このデータにはどの程度の鮮度が必要ですか? 参照テーブルなら staleTime を 1 時間にできます。リアルタイム追跡には、refetchInterval を設定した 5 秒の staleTime が必要です。設定をビジネス要件に合わせてください。
  • クエリのライフサイクルはどうなっていますか? 頻繁にアクセスされるデータには、より長い gcTime が必要です。一度だけ表示する詳細ビューでは、積極的にガベージコレクションできます。
オブザーバーのコスト
  • このクエリを購読するコンポーネントはいくつありますか? 1 つのキャッシュエントリーに対するオブザーバーが 10 を超える場合は、クエリを親コンポーネントに引き上げることを検討してください。100 を超える場合は、アーキテクチャ上の問題があることを示しています。
  • N 個のクエリを作成していますか、それとも N 個のオブザーバーを持つ 1 つのクエリを作成していますか? リスト項目には、個別の useQuery フックを呼び出させるのではなく、親クエリから props を渡してください。

使用方法

このスキルでは、コンテキストの使用量を最小限に抑えるため、段階的な開示を採用しています。状況に応じてリファレンスを読み込んでください。

シナリオ 1:クエリクライアントのセットアップ

必須 — ファイル全体を読む:サーバー/クライアントのセットアップパターンについて、query-client-setup.md(約 125 行)と server-integration.md(約 151 行)を最後まで読んでください。 初期セットアップでは、ほかのリファレンスを読み込まないでください。

本番環境向けの設定には assets/query-client.ts をコピーしてください。

シナリオ 2:クエリフックの作成
  1. 必須:キーのファクトリー設定について、query-keys.md(約 151 行)を読んでください。
  2. サーバーコンポーネントを使用する場合:server-integration.md を読んでください。
  3. ミューテーションを実装する場合を除き、mutations-and-updates.md は読み込まないでください。

設定値の決定には、以下の判断表を使用してください。

シナリオ 3:ミューテーションの実装

必須 — ファイル全体を読む:mutations-and-updates.md(約 345 行)を最後まで読んでください。ロールバックパターンについては、patterns-and-pitfalls.md を参照してください。 基本的な CRUD ミューテーションでは、caching-strategy.md を読み込まないでください。

シナリオ 4:パフォーマンス問題のデバッグ
  1. まず、以下のオブザーバー数のしきい値の表を確認してください(121~129 行)。
  2. オブザーバー数が 50 を超える場合:patterns-and-pitfalls.md を読んでください。
  3. 大規模なデータセットに関する問題の場合:構造共有について、fundamentals.md を読んでください。
  4. すべてのリファレンスを読み込まないでください。まず問題を診断し、その後、対象を絞って内容を読み込んでください。
シナリオ 5:多層キャッシュ戦略

必須:Next.js の use cache、TanStack Query、HTTP キャッシュを組み合わせたパターンについて、caching-strategy.md(約 198 行)を読んでください。 クライアント側の TanStack Query のみを使用する場合は、読み込まないでください。

クエリ設定の判断マトリクス

データの種類staleTimegcTimerefetchIntervalstructuralSharing備考
参照/ルックアップ1hrInfinity-true国、カテゴリ、静的な列挙値
ユーザープロフィール5min10min-true変更頻度が低く、鮮度は中程度
リアルタイム追跡5s30s5sfalse更新頻度が高く、ペイロードが大きい
ライブダッシュボード2s1min2sサイズによる鮮度とパフォーマンスのバランス
詳細ビュー30s2min-true必要に応じて取得し、キャッシュは中程度
検索結果1min5min-trueキャッシュ可能で、時間的な制約はない

ミューテーションパターンの選択

シナリオパターン使用する場面
フォーム送信悲観的複数ステップのフォーム、サーバーでの検証が必要な場合、先に進む前にエラーメッセージが必要な場合
トグル/チェックボックス楽観的二値状態の変更、低レイテンシが必要な場合、簡単にロールバックできる場合
ドラッグ&ドロップ楽観的即時の視覚的フィードバックが不可欠な場合、並べ替え操作、重要度の低いデータ
一括操作悲観的複数の項目を扱い、一部が失敗する可能性があり、成功した操作をユーザーに確認する必要がある場合
生命に関わる操作悲観的UI をサーバーの実際の状態に一致させる必要がある、医療、金融、安全性が重要なシステム
監査証跡が必要悲観的オペレーターの操作と記録されたイベントを正確に一致させる必要があるコンプライアンスシステム

クエリキーの設計

一貫性のある無効化のために、階層型ファクトリーを使用します。

// Recommended structure
export const keys = {
  all: () => ['domain'] as const,
  lists: () => [...keys.all(), 'list'] as const,
  list: (filters: string) => [...keys.lists(), filters] as const,
  details: () => [...keys.all(), 'detail'] as const,
  detail: (id: string) => [...keys.details(), id] as const,
};

// Invalidation examples
queryClient.invalidateQueries({ queryKey: keys.all() }); // Invalidate everything
queryClient.invalidateQueries({ queryKey: keys.lists() }); // Invalidate all lists
queryClient.invalidateQueries({ queryKey: keys.detail(id) }); // Invalidate one item

キーの安定性に関するルール:

  • 決定的なシリアライズ(結合する前に配列をソートする)
  • 時間に依存する値を使用しない(Date.now()、ランダムな ID)
  • 型の一貫性を保つ('1' と 1 を混在させない)
  • オブジェクトの形状を安定させる(キーをソートするか、シリアライズする)

サーバーとクライアントの統合パターン

レイヤー目的無効化方法キャッシュのスコープ
Next.js use cacheデータベースの負荷を軽減するrevalidateTag() または updateTag()リクエスト間、サーバー側
TanStack Queryクライアント側の状態管理queryClient.invalidateQueries()ブラウザーのタブごと
ブラウザーの HTTP キャッシュネットワークリクエストをなくすCache-Control ヘッダーブラウザーごと

統一された無効化戦略:

  1. サーバーとクライアントの両方のキャッシュで同じキー・ファクトリーを使用する
  2. サーバーのミューテーションでは updateTag(keys.detail(id).tag) を呼び出す
  3. クライアントのミューテーションでは queryClient.invalidateQueries({ queryKey: keys.detail(id) }) を呼び出す
  4. 両方のキャッシュを同じ階層で同期させる

オブザーバー数のしきい値

オブザーバー数パフォーマンスへの影響必要な対応
1-5無視できる程度なし
6-20最小限監視する。すぐに対応する必要はありません
21-50更新時に顕著クエリを親コンポーネントに引き上げることを検討する
51-100オーバーヘッドが大きいリファクタリングする:クエリを引き上げるか、select を使用する
100+重大な影響直ちにリファクタリングする:単一のクエリを使い、props を配布する

診断:

  1. 開発環境で TanStack Query DevTools を開く
  2. オブザーバー数の多いキャッシュエントリーを探す
  3. そのキーを持つ useQuery 呼び出しをコードベース内で検索する
  4. 親コンポーネントまたは共有キャッシュエントリーを使うようにリファクタリングする

クエリフックのパターン

パターン用途例
useSuspenseQueryServer Components との統合、Suspense 境界useSuspenseQuery({ queryKey, queryFn })
enabled を指定した useQuery依存クエリ、条件付きフェッチuseQuery({ queryKey, queryFn, enabled: !!userId })
select を指定した useQueryデータ変換、サブセットの選択useQuery({ queryKey, queryFn, select: selectFn }) — selectFn は安定したモジュールレベルの変数に抽出します。インライン関数はレンダーのたびに再実行されます
楽観的な useMutation低遅延の UI 更新、簡単に元に戻せる操作useMutation({ onMutate, onError, onSettled })
悲観的な useMutation重要度の高い操作、サーバーでの検証useMutation({ onSuccess })

よくあるエラーパターンと解決策

症状根本原因解決策解決しない場合の代替策
保存後にデータが更新されないクエリデータをuseStateにコピーしているクエリデータを直接使用し、useMemoで導出するrefetch()メソッドで強制的に再取得し、ネットワークタブで実際のAPIレスポンスを確認する
リクエストが無限に発生するクエリキーが不安定(Date.now()、ソートされていない配列)決定的なキー構築を使用する変更検知を追加する:const requestCount = useRef(0); useEffect(() => { requestCount.current++; if (requestCount.current > 10) console.error('Infinite loop detected', queryKey); }, [data]); キーの安定性パターンについてはfundamentals.mdを参照してください
同じリクエストがN回発生するリストアイテムごとにクエリを実行しているクエリを親に引き上げ、データをpropsで渡すすべてのコンポーネントで同一のqueryKey(同じオブジェクト参照または値)を使用していることを確認する:const queryKey = useMemo(() => keys.list(filters), [filters]); 高速なリクエストを重複排除するため、staleTimeを30秒に増やす
パラメーターがundefinedの状態でクエリが実行されるenabledガードがないenabled: Boolean(dependency)を追加するplaceholderDataを使って読み込み状態を表示し、queryFnに型ガードを追加して早期にエラーをスローする
リストの描画が遅いN個のクエリとN個のオブザーバー親で単一のクエリを実行し、propsで分配するselectを使って一部のみを購読し、仮想スクロールを実装してマウントされるコンポーネント数を減らす
キャッシュが削除されない頻繁に変化するデータにgcTime: Infinityを設定しているgcTimeをデータのライフサイクルに合わせるqueryClient.removeQueries()で強制的に削除し、DevToolsでキャッシュサイズを監視する
UIに古いデータが一瞬表示されるサーバーキャッシュが古く、クライアントキャッシュは新しい同じキーを使って無効化を統一するサーバーのpropsからinitialDataを使い、ハイドレーション済みのクエリではrefetchOnMount: falseを設定する
楽観的更新がロールバックされないonErrorでコンテキストを復元していないonErrorでonMutateのコンテキストを使うinvalidateQueriesで強制的に無効化し、以前の状態のスナップショットを使って手動でロールバックする
サーバーのハイドレーションで不一致が起きるSSRでタイムスタンプやユーザー固有のデータを使用しているコンテナにsuppressHydrationWarningを使うdynamic importとssr: falseによるクライアントのみの描画を使うか、タイムスタンプをUTCに正規化する
クエリが再取得されないenabled: falseガードによるブロック、またはgcTimeの期限切れenabledの条件を確認し、クエリがpredicateでフィルタリングされていないことを確認するgcTimeを増やしてキャッシュをより長く保持する。ポーリング動作にはrefetchIntervalを使う。staleTime: Infinityによってバックグラウンド再取得が妨げられていないか確認する
サーバーアクションで無効化されないupdateTag/revalidateTagとqueryClientで異なるキーを使っているサーバーとクライアントの両方のキャッシュで同じキー生成関数を使うサーバーアクションの後にrouter.refresh()を手動で呼び出し、タグ名がクエリキーの階層と一致することを確認する
ミューテーションは成功するがUIが更新されないonSuccessでの無効化がない、またはqueryKeyが間違っているonSuccess: () => queryClient.invalidateQueries({ queryKey })を追加するsetQueryDataを使ってキャッシュを手動で更新する:queryClient.setQueryData(keys.detail(id), newData)。queryKeyが完全に一致することを確認する

トラブルシューティングの決定木

パフォーマンスの問題

ステップ 1:DevTools でオブザーバー数を確認する(136~145 行目のしきい値を使用)

  • オブザーバーが 100 を超える → すぐにリファクタリングします。クエリを親コンポーネントに引き上げ、データを props 経由で配布します
  • オブザーバーが 51~100 → リファクタリングします。クエリを親コンポーネントに引き上げるか、select を使ってデータの一部を購読します
  • オブザーバーが 50 未満 → 問題は別の場所にあります。ステップ 2 に進みます

ステップ 2:データサイズと更新頻度を確認する

  • アイテムが 1000 件を超え、頻繁に更新される → 構造共有を無効にします:structuralSharing: false(詳細は fundamentals.md を参照)
  • ペイロードが大きい(>500KB) → ネットワークタブを確認し、ページネーションまたは無限クエリを検討します
  • 更新が速い(間隔 <1s) → staleTime を短くするか、refetchInterval を使用し、キャッシュ戦略を確認します

ステップ 3:React DevTools Profiler を確認する

  • クエリデータを使用するコンポーネントで、不要な再レンダリングが発生していないか確認します
  • select 関数がレンダリングごとに再作成されていないか確認します(useCallback を使用します)
  • 派生データの変換をインラインで行う代わりに、useMemo を使用すべきか確認します
  • コンポーネントのレンダリング時間をプロファイルして、ボトルネックを特定します
ネットワークの問題

不安定な接続:

  • リトライロジックを設定します:retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)
  • 本番環境でのリトライ設定については query-client-setup.md を参照してください

トークンの更新が必要な場合:

  • queryFn ラッパーに認証インターセプターパターンを実装します
  • グローバルな認証ヘッダーには queryClient.setQueryDefaults() を使用します
  • トークン更新のパターンについては patterns-and-pitfalls.md を参照してください

競合状態:

  • 無効化のタイミングを見直します。setQueryData の前に cancelQueries を使用します
  • 楽観的更新がバックグラウンドの再フェッチと競合していないか確認します
  • mutation の onMutate で await cancelQueries({ queryKey }) を使用していることを確認します
ハイドレーションの問題

SSR の不一致(コンソールにハイドレーションエラーが表示される):

  • コンテナ要素に suppressHydrationWarning を追加します
  • データを正規化します。サーバーとクライアントで同一の出力(安定したタイムスタンプ、ソート済みの配列)が生成されることを確認します
  • ユーザー固有のデータがサーバー側でレンダリングされていないか確認します

クライアントとサーバーのデータのずれ:

  • サーバー上のミューテーションで revalidateTag が実行されるタイミングを確認します
  • クライアントのキャッシュが最新である一方、サーバーのキャッシュ(Next.js use cache)が古くなっていないか確認します
  • サーバーの props から initialData を使用します:initialData: serverData, refetchOnMount: false

HydrationBoundary が機能しない:

  • クライアントコンポーネントの境界を確認します。HydrationBoundary は 'use client' コンポーネントをラップする必要があります
  • dehydratedState がサーバーから正しくシリアライズされているか確認します
  • ハイドレーション対象のクエリが shouldDehydrateQuery に含まれていることを確認します

自由度の調整

ミューテーションのリスクに合わせて、ガイダンスの具体性を調整します:

タスクの種類自由度ガイダンスの形式例
クエリの設定高い自由度一般的なパターンを示す表付きの原則「staleTime をビジネス要件に合わせる」
楽観的更新中程度の自由度ロールバック処理を含む完全なパターン「onMutate/onError/onSettled コールバックを使用する」
QueryClient の設定低い自由度重大なセキュリティ警告を含む正確なコード「サーバーでシングルトンを絶対に使用しないこと。ファクトリーを使用する」

判断基準:「エージェントが間違えた場合、どのような結果になりますか?」

  • サーバーでのシングルトンの誤用 → ユーザー間でのデータ漏洩(重大なセキュリティ上の問題)
  • オブザーバー数の誤認 → パフォーマンスの低下(中程度の影響)
  • staleTime の調整 → 最新性が最適でなくなる(影響は軽微)

重要な注意事項

  • クエリキーは決定論的にハッシュ化されます。['tracks', '1'] と ['tracks', 1] は異なるキャッシュエントリを作成します
  • ページ再読み込みやハイドレーションをまたいでキャッシュを永続化するには、クエリキーを JSON シリアライズ可能にする必要があります
  • pending ステータスを対象とする shouldDehydrateQuery を使用すると、サーバーコンポーネントで await せずにストリーミングできます
  • HydrationBoundary はクライアントコンポーネントのみをラップする必要があります。サーバーコンポーネントはこの境界を迂回します
  • revalidateTag と updateTag の使い分けは重要です。revalidateTag は stale-while-revalidate を使用し、updateTag は即座に無効化します
  • gcTime が期限切れになっていなければ、コンポーネントがマウントされていなくてもバックグラウンドの再フェッチが実行されます
  • select を使用する場合、構造共有は 2 回実行されます。1 回目は生データに対して、2 回目は変換後のデータに対して実行されます
  • select は正常にキャッシュされたデータに対してのみ実行され、エラー状態では呼び出されません。検証とエラーのスローは queryFn に記述してください
  • onMutate での cancelQueries は非常に重要です。バックグラウンドの再フェッチによって楽観的更新が上書きされる可能性があります
  • onMutate から返されたコンテキストは、ロールバック状態のために onError と onSettled に渡されます

コメント