# TanStack Query v5 実践ガイド【2026年最新】— 型安全なキャッシュ設計・楽観的更新・Next.js App Router 連携

> 公式ドキュメント最新版（v5.101 系）に忠実なTanStack Query実践ガイド。新しいMutationコールバック（context.client）、queryOptionsによる型安全設計、predicate無効化、楽観的更新の2手法、Next.js App Routerのプリフェッチ/ハイドレーションまで、本番品質のコード例で「いつ・どう使うか」を解説します。

- 公開日: 2026-06-24
- 著者: 友田 陽大
- タグ: React, TanStack Query, TypeScript, Next.js, 状態管理, パフォーマンス, フロントエンド
- URL: https://tomodahinata.com/blog/tanstack-query
- カテゴリ: フロントエンド
- 総合ガイド: https://tomodahinata.com/blog/nextjs-16-app-router-cache-components-data-fetching

## 要点

- TanStack Query はサーバー状態専用で、useState / Redux 等のクライアント状態管理とは役割が異なる
- v5 では Mutation コールバックに context が追加され、context.client から useQueryClient なしでキャッシュ操作できる
- queryOptions ヘルパーに定義を集約すれば、useQuery / prefetchQuery / getQueryData すべてに型が流れる
- 楽観的更新は表示1か所なら UI ベース、複数箇所はキャッシュベースで cancelQueries→退避→更新→onSettled同期
- App Router ではサーバーで prefetch し HydrationBoundary でハイドレーションするのが定石

---

この記事は公式ドキュメント（[Optimistic Updates](https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates)、[Mutations](https://tanstack.com/query/latest/docs/framework/react/guides/mutations)、[TypeScript](https://tanstack.com/query/latest/docs/framework/react/typescript)、[Advanced SSR](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr)）の最新版を一次情報として、実務で「どの場面で・どう書くか」を判断できるところまで踏み込みます。

---

## 1. 前提：なぜ TanStack Query なのか（サーバー状態 vs クライアント状態）

多くのバグは「サーバーから取得したデータ」を `useState` や Redux に**コピーして手で同期させようとする**ことから生まれます。リモートのデータは本質的に、

- 自分が所有していない（他のユーザーや別タブが書き換える）
- 取得した瞬間から古くなりうる（stale になる）
- 非同期で、失敗・再試行・キャンセルが起きる

という性質を持ちます。TanStack Query はこの「**サーバー状態**」をキャッシュとして一元管理し、再フェッチ・重複排除・バックグラウンド更新・エラー回復を自動化します。逆に「モーダルの開閉」「フォームの入力途中の値」などの**クライアント状態**は、引き続き `useState` や軽量ストアで持つのが正解です。役割を混同しないこと——これが設計の起点です。

### `staleTime` と `gcTime` の違い（最頻出の混乱ポイント）

| 設定         | 意味                                                     | デフォルト | 主な用途                                       |
| ------------ | -------------------------------------------------------- | ---------- | ---------------------------------------------- |
| `staleTime`  | データが「新鮮」とみなされる期間。この間は再フェッチしない | `0`        | 過剰なリクエストを抑える。SSRでは必須級         |
| `gcTime`     | 非アクティブなキャッシュをメモリから破棄するまでの時間     | 5分        | メモリ使用量とUX（戻った時の即表示）の調整      |

要点は「`staleTime` はネットワーク回数を、`gcTime` はメモリ寿命を制御する」こと。デフォルトの `staleTime: 0` は「マウントや再フォーカスのたびに裏で再フェッチ」を意味します。これは安全側の挙動ですが、SSR やコストを気にする画面では明示的に `staleTime` を引き上げます（後述の App Router で重要）。

> ここから先は基礎 API を理解している前提で、**本番運用に耐えるキャッシュ設計**に絞って解説します。

---

## 2. 【2026年の最新仕様】Mutation コールバックの新シグネチャと `context.client`

まず、多くの既存記事と差がつく最重要アップデートです。v5 の最新版では、Mutation のライフサイクルコールバックの引数が次のように整理されました。

```ts
onMutate:  (variables, context) => onMutateResult | Promise<onMutateResult | void>
onSuccess: (data, variables, onMutateResult, context) => unknown
onError:   (error, variables, onMutateResult, context) => unknown
onSettled: (data, error, variables, onMutateResult, context) => unknown
```

ポイントは2つです。

1. **末尾に `context`（`MutationFunctionContext`）が追加された。** ここには `context.client`（`QueryClient` 本体）・`context.meta`・`context.mutationKey` が入っています。つまり**コールバック内で `useQueryClient()` を呼ばなくても**、`context.client` からキャッシュ操作ができます。
2. **`onMutate` の戻り値は独立した `onMutateResult` 引数になった。** 以前は `onError(err, variables, context)` の第3引数がこの戻り値でしたが、名前が `onMutateResult` に明確化され、本物の `context` は第4引数に移りました。

### 既存コードは壊れるのか？

いいえ。`onError` / `onSettled` の**第1〜第3引数の位置は不変**（第3引数は従来どおり `onMutate` の戻り値）です。追加された第4引数の `context` を使わなければ、既存コードはそのまま動きます。新規実装では `context.client` を使うとコードがすっきりし、依存（`useQueryClient` のフック呼び出し）も減ります。

```tsx
// 旧来の書き方（今も動く）
const queryClient = useQueryClient();
useMutation({
  mutationFn: updateTodo,
  onSettled: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),
});

// 新シグネチャ：useQueryClient 不要、context.client を使う
useMutation({
  mutationFn: updateTodo,
  onSettled: (_data, _err, _vars, _onMutateResult, context) =>
    context.client.invalidateQueries({ queryKey: ["todos"] }),
});
```

この `context.client` を前提に、以降の楽観的更新を最新仕様で組み立てます。

---

## 3. 型安全の起点：`queryOptions` ヘルパーとクエリキー設計

公式が TypeScript ガイドで最初に勧めるのが **`queryOptions` ヘルパー**です。クエリの定義（`queryKey` + `queryFn` + オプション）を1か所に集約し、`useQuery` / `prefetchQuery` / `getQueryData` のすべてに**型と設定を流す**ための仕組みです。これが DRY（単一の正）と型安全の両方を同時に解決します。

```ts
// lib/todos/queries.ts
import { queryOptions } from "@tanstack/react-query";
import { fetchTodos, fetchTodo } from "./api";
import type { Todo, TodoFilters } from "./types";

// クエリキーファクトリ：キーの命名を一元管理し、タイポと不整合を排除する
export const todoKeys = {
  all: ["todos"] as const,
  lists: () => [...todoKeys.all, "list"] as const,
  list: (filters: TodoFilters) => [...todoKeys.lists(), filters] as const,
  details: () => [...todoKeys.all, "detail"] as const,
  detail: (id: number) => [...todoKeys.details(), id] as const,
};

// queryOptions に定義を集約。戻り値は型情報を保持する
export const todoListOptions = (filters: TodoFilters) =>
  queryOptions({
    queryKey: todoKeys.list(filters),
    queryFn: () => fetchTodos(filters),
    staleTime: 60_000,
  });

export const todoDetailOptions = (id: number) =>
  queryOptions({
    queryKey: todoKeys.detail(id),
    queryFn: () => fetchTodo(id),
  });
```

呼び出し側は1行。しかも `getQueryData` が**戻り値の型を自動で知っている**ため、ジェネリクスの手書きが不要になります。

```tsx
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { todoListOptions, todoKeys } from "@/lib/todos/queries";

function TodoList({ filters }: { filters: TodoFilters }) {
  const { data } = useQuery(todoListOptions(filters)); // data: Todo[] | undefined（自動推論）
  // ...
}

// キャッシュ直接読み取りも型安全
const queryClient = useQueryClient();
const cached = queryClient.getQueryData(todoListOptions(filters).queryKey);
//    ^? Todo[] | undefined  — ジェネリクス指定なしで型がつく
```

### なぜこれが効くのか

- **クエリキーの構造**（`["todos", "list", filters]` という階層）が `todoKeys` に集約され、`invalidateQueries({ queryKey: todoKeys.lists() })` のように**プレフィックス無効化が意図どおり**効きます（後述）。
- 文字列リテラルのキー直書きが消え、リファクタ耐性が上がります（ETC: Easy To Change）。
- `staleTime` などの設定も定義側に集約されるため、サーバーとクライアントで設定がズレません。

> エラー型について：`error` はデフォルトで `Error` 型です。アプリ全体で統一したい場合は `Register` インターフェースで `defaultError` をグローバル登録できます。安易な `unknown` 放置や `any` キャストは避け、`AxiosError` などは型ナローイングで絞り込みます。

---

## 4. 外科手術的なキャッシュ無効化：`invalidateQueries` の精密制御

`invalidateQueries({ queryKey: ["todos"] })` は便利ですが、`["todos"]` で**始まるすべて**のクエリ（`["todos", "list"]`、`["todos", "detail", 1]` …）を一括で stale 化し、アクティブなものを再フェッチします。意図しない一斉再フェッチはパフォーマンスの落とし穴です。次の4つを使い分けて「必要な範囲だけ」無効化します。

### 4-1. マッチング戦略（prefix / 完全一致 / 変数指定 / predicate）

```tsx
// プレフィックス一致：["todos"] で始まるすべて
queryClient.invalidateQueries({ queryKey: todoKeys.all });

// 変数まで指定：特定フィルタのリストだけ
queryClient.invalidateQueries({ queryKey: todoKeys.list({ type: "done" }) });

// 完全一致：そのキー自身のみ（子孫は対象外）
queryClient.invalidateQueries({ queryKey: todoKeys.all, exact: true });

// predicate：各クエリインスタンスを評価する最も柔軟な方法
queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey[0] === "todos" &&
    typeof query.queryKey[1] === "object" &&
    (query.queryKey[1] as { version?: number }).version! >= 10,
});
```

### 4-2. ドメインロジックで無効化する `predicate`

`predicate` はクエリキーだけでなく `query.meta` など**インスタンスの状態**で判定できます。「機密データだけログアウト時に破棄する」のような、キー構造に依存しない無効化が書けます。

```tsx
// 定義側：meta でドメインの意味づけを付与
export const useSensitiveTodos = () =>
  useQuery({
    queryKey: todoKeys.list({ type: "sensitive" }),
    queryFn: fetchSensitiveTodos,
    meta: { cacheType: "sensitive" },
  });

// ログアウト時：meta が sensitive のクエリだけを無効化
function handleLogout() {
  queryClient.invalidateQueries({
    predicate: (query) => query.meta?.cacheType === "sensitive",
  });
}
```

### 4-3. `refetchType` で「いつ再フェッチするか」を制御する

`invalidateQueries` はデフォルト（`refetchType: "active"`）では**今マウントされているクエリだけ**を再フェッチし、非アクティブなものは「次にマウントされた時」に回します。先読みしたい・抑えたいを明示できます。

| `refetchType` | 挙動                                                   | 使いどころ                                       |
| ------------- | ------------------------------------------------------ | ------------------------------------------------ |
| `"active"`（既定） | 表示中のクエリのみ即再フェッチ                       | 通常はこれで十分                                 |
| `"inactive"`  | 非表示のクエリのみ再フェッチ                          | 裏で別画面を先に温めたい                          |
| `"all"`       | 表示・非表示すべて再フェッチ                          | 遷移先のローディングを消したい先読み             |
| `"none"`      | stale 化するが再フェッチしない                        | コスト最優先。次回アクセス時にまとめて更新        |

```tsx
// 設定更新後、非表示のダッシュボードも裏で温めておく（遷移時のスピナーを消す）
queryClient.invalidateQueries({ queryKey: ["dashboard"], refetchType: "all" });
```

---

## 5. 楽観的更新（Optimistic Updates）— 公式が示す2つの正攻法

楽観的更新は「サーバー応答を待たずに UI を即更新」する手法で、UX を劇的に改善します。**v5 の公式ドキュメントは2つのアプローチを提示**しており、どちらを選ぶかは「更新結果を画面の何か所で見せるか」で決まります。

### 5-1. UI ベース（推奨：表示箇所が1か所なら最小コード）

キャッシュを書き換えず、`useMutation` が返す `variables`（送信中の入力値）を**そのまま仮表示**する方法です。コードが圧倒的に少なく、ロールバック処理が不要です。

```tsx
function AddTodo() {
  const { mutate, isPending, variables, isError } = useMutation({
    mutationFn: (text: string) => createTodo(text),
    onSettled: () =>
      queryClient.invalidateQueries({ queryKey: todoKeys.lists() }),
  });

  return (
    <>
      <ul>
        {todos.map((t) => (
          <li key={t.id}>{t.text}</li>
        ))}
        {/* 送信中は variables を半透明で仮表示。失敗時は再送ボタンを出す */}
        {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}
        {isError && (
          <li style={{ color: "red" }}>
            {variables}{" "}
            <button onClick={() => mutate(variables!)}>再試行</button>
          </li>
        )}
      </ul>
      <button onClick={() => mutate("新しいタスク")}>追加</button>
    </>
  );
}
```

別コンポーネントからこの「送信中の値」を参照したい場合は、`mutationKey` を付けて `useMutationState` で拾います。

```tsx
const pendingTexts = useMutationState<string>({
  filters: { mutationKey: ["addTodo"], status: "pending" },
  select: (mutation) => mutation.state.variables as string,
});
```

### 5-2. キャッシュベース（複数箇所に反映するならこちら）

「リスト・詳細・バッジなど**複数の場所**に即時反映したい」場合は、キャッシュを直接書き換えます。ここで**安全なロールバック**が要点になります。新シグネチャ（`context.client` / `onMutateResult`）で書くとこうなります。

```tsx
// lib/todos/mutations.ts
import { useMutation } from "@tanstack/react-query";
import { updateTodo } from "./api";
import { todoKeys } from "./queries";
import type { Todo, UpdateTodoInput } from "./types";

export const useToggleTodo = () =>
  useMutation({
    mutationFn: (input: UpdateTodoInput) => updateTodo(input),

    // ① Mutation 実行直前。context.client から QueryClient にアクセス
    onMutate: async (input, context) => {
      const key = todoKeys.lists();

      // 1-1. 進行中の再フェッチを止める（楽観更新がサーバー応答で上書きされるのを防ぐ）
      await context.client.cancelQueries({ queryKey: key });

      // 1-2. ロールバック用に現在のキャッシュを退避
      const previousTodos = context.client.getQueryData<Todo[]>(key);

      // 1-3. キャッシュをイミュータブルに楽観更新
      context.client.setQueryData<Todo[]>(key, (old) =>
        (old ?? []).map((todo) =>
          todo.id === input.id ? { ...todo, ...input } : todo,
        ),
      );

      // 1-4. 戻り値は onMutateResult として onError / onSettled に渡る
      return { previousTodos };
    },

    // ② 失敗時：退避しておいたスナップショットへ確実に戻す
    onError: (_err, _input, onMutateResult, context) => {
      if (onMutateResult?.previousTodos) {
        context.client.setQueryData(todoKeys.lists(), onMutateResult.previousTodos);
      }
    },

    // ③ 成否に関わらず、最後はサーバーの「正」と同期する
    onSettled: (_data, _err, _input, _onMutateResult, context) => {
      context.client.invalidateQueries({ queryKey: todoKeys.lists() });
    },
  });
```

なぜこの順序が「本番品質」なのか：

- **`cancelQueries` を先頭で呼ぶ理由**：これを省くと、楽観更新の直後に走った再フェッチが古いデータで UI を上書きし、「更新 → 一瞬戻る → 応答で再更新」というちらつき（flicker）が出ます。
- **スナップショットを `onMutateResult` 経由で渡す理由**：`onError` 内で改めて `getQueryData` を呼ぶと、**すでに楽観更新された後**の値を拾ってしまいます。`onMutate` 開始時点の値を退避しておくことだけが、確実なロールバックを保証します。
- **`onSettled` で必ず無効化する理由**：成功・失敗どちらでも最後にサーバーと同期することで、楽観更新の失敗が画面に残り続ける事故を防ぎます（回復性の担保）。

### どちらを使う？（判断基準）

| 状況                                           | 推奨アプローチ          |
| ---------------------------------------------- | ----------------------- |
| 更新結果を見せるのが1か所だけ                 | UI ベース（5-1）        |
| 同じ更新を複数の画面・コンポーネントに反映     | キャッシュベース（5-2） |
| ロールバックの複雑さを避けたい                 | UI ベース（5-1）        |
| 詳細・一覧・集計など派生データの整合が必要      | キャッシュベース（5-2） |

---

## 6. API を介さないキャッシュ直接更新：`setQueryData`

サーバー状態を変えない「純粋なクライアント側の表示変更」——並び替え、モーダル内の一時編集など——は、`useMutation` を使うのは過剰です。`setQueryData` で直接キャッシュを書き換えるのが正解です。

```tsx
import { arrayMove } from "@dnd-kit/sortable";
import type { DragEndEvent } from "@dnd-kit/core";

function handleDragEnd(event: DragEndEvent) {
  const { active, over } = event;
  if (!over || active.id === over.id) return;

  // updater 関数を渡し、必ずイミュータブルに新しい配列を返す
  queryClient.setQueryData<Todo[]>(todoKeys.lists(), (old) => {
    if (!old) return old;
    const from = old.findIndex((t) => t.id === active.id);
    const to = old.findIndex((t) => t.id === over.id);
    return arrayMove(old, from, to); // 非破壊的に並び替え
  });
}
```

押さえるべき原則：

- **`useMutation` との使い分け**：これはサーバーの「更新」ではなく表示状態の変更です。`useMutation` を使うと不要な `isPending` 状態や `mutationKey` 管理を招き、意味論的にも誤りです。
- **必ず updater 関数を使う**：`setQueryData(key, newData)` で値を直接渡すのではなく `setQueryData(key, (old) => ...)` を使うと、競合状態（直前の別更新との衝突）を避けられます。
- **イミュータブルに**：React の state 更新と同じく、`old` を破壊せず新しい参照を返すこと。これで TanStack Query が変更を検知し、必要なコンポーネントだけ再レンダリングします。

---

## 7. Next.js App Router 連携：サーバープリフェッチ + ハイドレーション

App Router 環境では「**Server Component でプリフェッチ → `HydrationBoundary` でクライアントに引き渡す**」のが公式の定石です。初回表示は SSR で速く、以降はクライアントのキャッシュとして TanStack Query の全機能（再フェッチ・楽観更新）が使えます。

### 7-1. リクエストごとに分離する `getQueryClient`

サーバーでは**リクエストごとに新しい `QueryClient`** を作り（ユーザー間のデータ混在を防ぐ）、ブラウザでは**シングルトン**を再利用します。判定には公式が export する `isServer` を使います。

```ts
// app/get-query-client.ts
import { isServer, QueryClient } from "@tanstack/react-query";

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      // ハイドレーション直後の即時再フェッチを防ぐ。SSR では実質必須
      queries: { staleTime: 60 * 1000 },
    },
  });
}

let browserQueryClient: QueryClient | undefined;

export function getQueryClient() {
  if (isServer) return makeQueryClient(); // リクエストごとに新規生成
  if (!browserQueryClient) browserQueryClient = makeQueryClient();
  return browserQueryClient; // ブラウザでは使い回す
}
```

### 7-2. Server Component でプリフェッチ → dehydrate

`queryOptions`（第3章）をそのまま渡せるのがポイント。**サーバーのプリフェッチとクライアントの `useQuery` で同じ定義を共有**でき、キーと `queryFn` の二重管理が消えます。

```tsx
// app/todos/page.tsx（Server Component）
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { getQueryClient } from "@/app/get-query-client";
import { todoListOptions } from "@/lib/todos/queries";
import { TodoList } from "./todo-list";

export default async function TodosPage() {
  const queryClient = getQueryClient();

  // 同じ queryOptions を使い回す（DRY）
  await queryClient.prefetchQuery(todoListOptions({ type: "done" }));

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <TodoList />
    </HydrationBoundary>
  );
}
```

```tsx
// app/todos/todo-list.tsx（Client Component）
"use client";
import { useQuery } from "@tanstack/react-query";
import { todoListOptions } from "@/lib/todos/queries";

export function TodoList() {
  // プリフェッチ済みのキャッシュから即座に描画される
  const { data } = useQuery(todoListOptions({ type: "done" }));
  return (
    <ul>
      {data?.map((t) => (
        <li key={t.id}>{t.text}</li>
      ))}
    </ul>
  );
}
```

### 7-3. ストリーミング：`await` せずに流す

すべてのプリフェッチ完了を待たずに HTML を流したい場合は、`shouldDehydrateQuery` で **pending 状態のクエリも dehydrate** するよう設定し、`prefetchQuery` を `await` せずに呼びます。データが解決し次第クライアントへストリームされます。

```ts
// get-query-client.ts の defaultOptions に追加
import { defaultShouldDehydrateQuery } from "@tanstack/react-query";

dehydrate: {
  shouldDehydrateQuery: (query) =>
    defaultShouldDehydrateQuery(query) || query.state.status === "pending",
}
```

> 注意：プリフェッチした同じデータを Server Component と Client Component の**両方でレンダリングしない**こと（同期ズレの温床）。Server Component は「データ取得層」、表示はクライアントに寄せると破綻しません。

---

## 8. Suspense との統合：`useSuspenseQuery`

ローディング/エラーの分岐を `<Suspense>` と Error Boundary に**外出し**したいなら `useSuspenseQuery` を使います。最大の利点は **`data` が常に定義済み**（`Todo[]`、`undefined` を含まない）になり、`if (isPending)` の分岐が消えてコードが直線的になることです。

```tsx
"use client";
import { useSuspenseQuery } from "@tanstack/react-query";
import { todoListOptions } from "@/lib/todos/queries";

function TodoList({ filters }: { filters: TodoFilters }) {
  // data は Todo[]（undefined ではない）。isPending / error の分岐は不要
  const { data } = useSuspenseQuery(todoListOptions(filters));
  return (
    <ul>
      {data.map((t) => (
        <li key={t.id}>{t.text}</li>
      ))}
    </ul>
  );
}
```

押さえどころ：

- ローディングは親の `<Suspense fallback={...}>`、エラーは Error Boundary が担当します。
- デフォルトのエラー送出は「**表示できる他データが無い時だけ throw**」（`throwOnError: (e, q) => typeof q.state.data === "undefined"`）。古いキャッシュがあれば、それを見せたまま裏で再試行します。
- `placeholderData` は使えません。更新時のフォールバック表示を避けたい場合は `startTransition` で更新を包みます。
- Error Boundary をリセットして再試行させるには `QueryErrorResetBoundary` / `useQueryErrorResetBoundary` を使います。

---

## 9. 本番運用のためのベストプラクティス＆アンチパターン

| やるべきこと（Do）                                              | 避けるべきこと（Don't）                                       |
| --------------------------------------------------------------- | ------------------------------------------------------------ |
| `queryOptions` + クエリキーファクトリで定義を一元化            | キーを文字列直書きして各所に散らす                            |
| SSR/App Router では `staleTime` を明示（例 60s）               | `staleTime: 0` のままハイドレーション直後に二重フェッチ       |
| 楽観更新は `cancelQueries` → 退避 → 更新 → `onSettled`同期      | `onError` 内で `getQueryData` し直してロールバックする        |
| サーバー状態は Query、UI状態は `useState`/軽量ストア            | サーバーデータを `useState` にコピーして手動同期する          |
| エラー型は `Error` か `Register` で統一、境界で型ナローイング   | `catch (e: any)` や `as` キャストで握りつぶす                 |
| 純粋な表示変更は `setQueryData(updater)`                        | 表示順の変更に `useMutation` を使う                           |
| `invalidateQueries` は `exact`/`predicate`/`refetchType`で限定 | 親キーで一括無効化し、無関係なクエリまで再フェッチ            |

可観測性の観点では、開発時に [React Query Devtools](https://tanstack.com/query/latest/docs/framework/react/devtools) を入れておくと、キャッシュ状態・stale 判定・再フェッチの発火が可視化され、上記の事故を未然に検知できます。

---

## 10. FAQ（よくある質問）

**Q. `staleTime` と `gcTime` の違いは？**
A. `staleTime` は「データを新鮮とみなす期間（＝再フェッチを抑える）」、`gcTime` は「非アクティブなキャッシュをメモリ保持する期間」です。前者はネットワーク回数、後者はメモリ寿命を制御します。

**Q. `invalidateQueries` と `setQueryData` と `refetch` の使い分けは？**
A. サーバーと再同期したいなら `invalidateQueries`（stale 化＋再フェッチ）、サーバーを呼ばずキャッシュを直接書き換えるなら `setQueryData`、特定クエリを今すぐ取り直すなら `refetch` です。

**Q. TanStack Query は Redux / Zustand の代わりになりますか？**
A. 「サーバー状態」については代替になります（むしろ最適）。一方、モーダル開閉やフォーム途中値などの「クライアント状態」は引き続き `useState` や軽量ストアが適任です。両者は競合せず役割分担します。

**Q. Next.js の Server Components があるのに TanStack Query は必要？**
A. 初回表示は Server Components で十分なことも多いですが、**クライアント側での再フェッチ・楽観更新・ポーリング・無限スクロール・キャッシュ共有**が必要なら TanStack Query が有効です。プリフェッチ＋ハイドレーションで両者は綺麗に両立します。

**Q. 楽観的更新は UI ベースとキャッシュベース、どちらを選ぶ？**
A. 更新結果を見せる箇所が1つなら UI ベース（`variables`）が最小コスト。複数箇所に同時反映するならキャッシュベース（`onMutate` + `setQueryData`）です。

**Q. React 版に v6 はありますか？**
A. 2026年6月時点で React 版は v5 が最新です。「v6」は Svelte 用アダプタを指し、コアは v5 を共有しています。本記事のコードは v5 最新仕様に準拠しています。

---

## まとめ：キャッシュは「アプリの現在の状態」である

TanStack Query v5 を使いこなす鍵は、キャッシュを「取得したデータの置き場」ではなく「**アプリの正となる状態ストア**」として能動的に設計することです。本記事の柱を振り返ると——

1. **`context.client` を使う新シグネチャ**で、Mutation のコールバックを簡潔・安全に書く。
2. **`queryOptions` + クエリキーファクトリ**で、定義を一元化し型をすべてに流す。
3. **`predicate` / `refetchType`** でネットワークを外科手術的に制御する。
4. **楽観的更新は2手法を使い分け**、`cancelQueries` → 退避 → 更新 → `onSettled` 同期で整合性を担保する。
5. **App Router ではプリフェッチ＋ハイドレーション**、必要に応じて **Suspense** で UI を直線化する。

これらは単なる小手先のテクニックではなく、可読性・型安全性・回復性・保守性を同時に引き上げる「設計の選択」です。正しく適用すれば、ユーザー体験（即応性）と開発体験（壊れにくさ）の両方が底上げされます。

実際のプロダクトでは、ここに「サーバー側のべき等性」「リトライ戦略」「監視・アラート」まで含めて初めて本番品質になります。**こうした本番運用に耐えるフロントエンド設計やレビュー、既存コードの改善が必要な場合は、お気軽にご相談ください。** 下記の事例では、業界の基幹業務を支える B2B SaaS を、型安全・回復性・保守性を重視して設計・実装した過程を紹介しています。
