Use Cases (更新: 2026/6/6)

TanStack Queryを「ただ動く」で終わらせない。Claude Code流キャッシュ設計

「データ取得作って」だけで頼むとキャッシュが崩れる。query key設計・無効化・楽観的更新を、Claude Codeに正しく任せる手順をv5の動くコードで解説。

TanStack Queryを「ただ動く」で終わらせない。Claude Code流キャッシュ設計

「TanStack Queryでprojects一覧の取得を作って」

そうClaude Codeに頼んだら、5分でちゃんと動く画面が出てきました。一覧が出る、検索もできる、見た目は完璧。やるじゃん、と思った。

問題はその翌週でした。検索条件を変えるたびにキャッシュが別物として増殖し、編集して戻ると一覧だけ古いまま。staleTimeの数字には理由が書かれておらず、レビューで「なんで60秒?」と聞かれて僕は答えられませんでした。

動いているのに、誰も中身を説明できない。これがTanStack Queryでいちばんよくある事故です。原因はモデルの賢さじゃなくて、最初に渡した「設計の枠」が足りなかったこと。今日はその枠の作り方を、コピペで動くv5コードと一緒に書きます。

この記事の要点

  • TanStack Queryが扱うのはサーバー状態。フォーム入力やモーダル開閉のようなUI状態とは分けて、最初にClaude Codeへ境界を渡す。
  • 事故の9割はquery keyから来る。「キャッシュの住所」として配列で設計し、検索条件は正規化してから入れる。
  • mutationは「成功時」だけでなく失敗時にcacheを戻すところまでワンセットで依頼する。onMutateonErroronSettledの型を崩さない。
  • cacheTimeはv5で**gcTimeに名前が変わった**。古い記事のコピペは事故のもと。
  • 依頼の順番は「query key factoryを先に固定→UIは最後」。逆にすると正規化漏れとSSRのkey不一致が後から噴き出す。

まずサーバー状態とUI状態の線を引く

TanStack Queryが面倒を見るのは、基本的にサーバー状態です。サーバー状態というのは、ブラウザではなくAPIやDBのほうが「正解」を持っているデータのこと。プロジェクト一覧、ユーザー情報、請求データ。これらは自分のアプリが勝手に決めていい値ではありません。

一方で、検索フォームの入力途中、モーダルの開閉、選択中のタブ。こういう一時的なUI状態は、TanStack Queryに入れちゃダメです。混ぜると「キャッシュなのか画面の状態なのか」が誰にも分からなくなります。クライアント状態の置き場に迷ったらClaude Code Zustandでクライアント状態を管理するのほうで整理してください。

Claude Codeに頼む前に、僕はいつもこの表を埋めます。口頭で「いい感じに」と言うより、表で渡したほうが出力が驚くほど安定します。

決めることレビューで見る理由
query keyの単位["projects", "list", filters]キャッシュの住所がぶれない
データの鮮度一覧は60秒、詳細は5分何を再取得するか説明できる
mutation後の処理一覧prefixと詳細keyを無効化古いUIを残さない
楽観的更新ステータス切替だけ先に反映失敗時に戻せる
検証方法Vitest、MSW、build差分を証拠で確認できる

この5行を埋めずにコードを書かせると、あとで全部レビューで聞き直すことになります。先に決めておけば、Claude Codeの差分は小さくなり、レビューは「どのキャッシュを意図的に触ったか」だけに集中できます。

query keyは「キャッシュの住所」だと考える

つまずきポイントは、ほぼ全部ここに集まります。query keyを軽く考えると、必ず後で泣きます。

公式のQuery Keysでは、query keyはトップレベルが配列で、JSON.stringifyできて、対象データに対して一意であること、と定義されています。要するに、同じデータには同じ住所、違うデータには違う住所を割り当てる仕組みです。

ここで僕がやらかしたのは、画面名をkeyに入れたことでした。["projectListPage"] みたいに。これだと検索条件を変えても住所が同じなので、別の検索結果が同じ箱に上書きされてしまう。逆に、正規化せずに { search: " Billing " } をそのまま入れると、" Billing ""billing" が別の住所になり、同じ一覧なのにキャッシュが二重に増えます。

正解は、画面名ではなくデータの条件をkeyに入れ、その条件を入れる前に正規化することです。

具体的なユースケースで考えると分かりやすいです。

  1. 一覧画面: 検索語・ステータス・ページ番号をkeyに入れる。「URLに出る条件だけをkeyに入れる」「空文字は揃える」とClaude Codeに指示すると、同じ一覧が別キャッシュになる事故が減ります。
  2. 詳細・編集画面: 詳細は ["projects", "detail", id] で持ち、編集が成功したら詳細keyと一覧prefixの両方を無効化する。片方忘れると「編集後に一覧だけ古い」が起きます。
  3. ステータス切替・お気に入り: クリック直後にUIを変えたいので楽観的更新を使う。ただし「先に変える」だけでは危険で、戻す処理までセットにします。
  4. SSR・Next.js App Routerの初期表示: サーバーでprefetchしたデータをクライアントへ渡す。このときサーバーとクライアントでkeyの形がズレると、表示直後に二重fetchします。

query keyとstaleTime/gcTimeを実装する

実際の依頼文はこう書きます。専門用語も短く定義して渡すと、レビュー観点が揃います。

既存のReact + TypeScriptコードを読んで、TanStack Query v5でprojects一覧を実装してください。
query keyはトップレベル配列、filtersは正規化、一覧はstaleTime 60秒、gcTime 10分。
queryOptionsを使い、useQuery側ではplaceholderData: keepPreviousDataでページ切替のちらつきを抑えてください。
API関数、型、query key factory、hookを1ファイルにまとめ、cacheTimeという名前は使わないでください。

ここで cacheTime を禁止しているのは理由があります。v5で cacheTimegcTime に名前が変わりました。ネット上の古い記事をそのまま貼ると、動かないか、warningが出ます。staleTime は「fresh(新鮮)として扱う時間」、gcTime は「使われなくなったクエリがキャッシュに残る時間」。この2つの違いを、後で必ず聞かれます。デフォルトの挙動は公式のImportant Defaultsに書かれています。

次のコードを src/features/projects/projects.query.ts として貼り付けます。

import {
  keepPreviousData,
  queryOptions,
  useQuery,
} from "@tanstack/react-query";

export type ProjectStatus = "all" | "active" | "paused";
export type EditableProjectStatus = Exclude<ProjectStatus, "all">;

export type Project = {
  id: string;
  name: string;
  status: EditableProjectStatus;
  updatedAt: string;
};

export type ProjectFilters = {
  status: ProjectStatus;
  search: string;
  page: number;
};

export type ProjectListResponse = {
  items: Project[];
  page: number;
  hasMore: boolean;
};

export type UpdateProjectStatusInput = {
  id: string;
  status: EditableProjectStatus;
};

// 検索条件をそろえる。" Billing " と "billing" を別キャッシュにしないため
function normalizeFilters(filters: ProjectFilters): ProjectFilters {
  return {
    status: filters.status,
    search: filters.search.trim().toLowerCase(),
    page: Math.max(1, filters.page),
  };
}

async function readJson<T>(response: Response): Promise<T> {
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  return (await response.json()) as T;
}

export async function fetchProjects(
  filters: ProjectFilters,
): Promise<ProjectListResponse> {
  const normalized = normalizeFilters(filters);
  const params = new URLSearchParams({
    status: normalized.status,
    search: normalized.search,
    page: String(normalized.page),
  });

  const response = await fetch(`/api/projects?${params.toString()}`);
  return readJson<ProjectListResponse>(response);
}

export async function fetchProject(id: string): Promise<Project> {
  const response = await fetch(`/api/projects/${id}`);
  return readJson<Project>(response);
}

export async function updateProjectStatus(
  input: UpdateProjectStatusInput,
): Promise<Project> {
  const response = await fetch(`/api/projects/${input.id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ status: input.status }),
  });

  return readJson<Project>(response);
}

// query key factory。住所の付け方をここ1か所に集約する
export const projectKeys = {
  all: ["projects"] as const,
  lists: () => [...projectKeys.all, "list"] as const,
  list: (filters: ProjectFilters) =>
    [...projectKeys.lists(), normalizeFilters(filters)] as const,
  details: () => [...projectKeys.all, "detail"] as const,
  detail: (id: string) => [...projectKeys.details(), id] as const,
};

export const projectQueries = {
  list: (filters: ProjectFilters) =>
    queryOptions({
      queryKey: projectKeys.list(filters),
      queryFn: () => fetchProjects(filters),
      staleTime: 60_000, // 一覧は60秒freshとして扱う
      gcTime: 10 * 60_000,
    }),
  detail: (id: string) =>
    queryOptions({
      queryKey: projectKeys.detail(id),
      queryFn: () => fetchProject(id),
      staleTime: 5 * 60_000, // 詳細は更新頻度が低いので長め
      gcTime: 30 * 60_000,
    }),
};

export function useProjects(filters: ProjectFilters) {
  return useQuery({
    ...projectQueries.list(filters),
    placeholderData: keepPreviousData,
  });
}

このコードのキモは、normalizeFilters をkeyに入れる前に必ず通している点です。これがあるだけで、検索バーの前後の空白や大文字小文字が原因の「キャッシュ二重化」が消えます。あと、staleTime: Infinity を最初から乱用しないこと。別の人が同じデータを更新する管理画面では、古い情報を永遠にfresh扱いするほうが、よっぽど危険な場面があります。

Reactの構成そのものをどう分けるかはClaude CodeでReact開発を進めるで別に書いています。

mutationは「失敗したら戻す」までを1セットで頼む

mutationは「サーバーへ変更を送る操作」です。ここでいちばんやりがちなのが、成功した時のことしか頼まないこと。ネットが遅い、サーバーが500を返す、そういう時にUIだけ先に変わって戻らないと、ユーザーは「成功したのに消えた」と混乱します。

公式のOptimistic Updatesが示す流れはこうです。onMutate で進行中のクエリをキャンセルし、現状をスナップショットし、UIを先に書き換える。onError で元に戻す。onSettled で無効化する。この4段をバラバラにせず、毎回まとめて依頼します。

src/features/projects/useUpdateProjectStatus.ts を追加します。

import type { QueryKey } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
  projectKeys,
  updateProjectStatus,
  type Project,
  type ProjectListResponse,
} from "./projects.query";

type ListSnapshot = [QueryKey, ProjectListResponse | undefined];

export function useUpdateProjectStatus() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: updateProjectStatus,
    onMutate: async (input) => {
      // 進行中のrefetchを止める。後勝ちで古い値に上書きされないように
      await queryClient.cancelQueries({ queryKey: projectKeys.lists() });
      await queryClient.cancelQueries({ queryKey: projectKeys.detail(input.id) });

      // 失敗したとき戻せるよう、今の状態を控えておく
      const listSnapshots =
        queryClient.getQueriesData<ProjectListResponse>({
          queryKey: projectKeys.lists(),
        }) as ListSnapshot[];
      const detailSnapshot = queryClient.getQueryData<Project>(
        projectKeys.detail(input.id),
      );

      // UIを先に書き換える(楽観的更新)
      for (const [key, data] of listSnapshots) {
        if (!data) continue;

        queryClient.setQueryData<ProjectListResponse>(key, {
          ...data,
          items: data.items.map((project) =>
            project.id === input.id
              ? { ...project, status: input.status }
              : project,
          ),
        });
      }

      queryClient.setQueryData<Project>(
        projectKeys.detail(input.id),
        (current) =>
          current ? { ...current, status: input.status } : current,
      );

      return { listSnapshots, detailSnapshot };
    },
    onError: (_error, input, context) => {
      // 控えておいた状態に巻き戻す
      for (const [key, data] of context?.listSnapshots ?? []) {
        queryClient.setQueryData(key, data);
      }

      queryClient.setQueryData(projectKeys.detail(input.id), context?.detailSnapshot);
    },
    onSettled: async (_data, _error, input) => {
      // 成功でも失敗でも、最後にサーバーと突き合わせる
      await Promise.all([
        queryClient.invalidateQueries({ queryKey: projectKeys.lists() }),
        queryClient.invalidateQueries({ queryKey: projectKeys.detail(input.id) }),
      ]);
    },
  });
}

落とし穴は2つ。詳細だけ更新して一覧を忘れるか、逆に一覧を全部無効化して不要な再取得を増やすか。上の例では projectKeys.lists() というprefixを使って「一覧系だけ」を狙い撃ちしています。invalidateQueries がやってくれることは公式のQuery Invalidationに書かれていて、対象のクエリをstale扱いにし、表示中ならバックグラウンドで取り直します。

請求や監査ログのように影響範囲が広い機能では、僕はClaude Codeに「どのquery keyを無効化したかをPR本文に箇条書きして」と頼みます。これだけでレビューが一気にラクになります。

loading・error・更新中を画面で出し分ける

UIでは isPendingisErrorisFetching、それと mutation 側の isPending をきっちり分けます。初回ロード、裏での再取得、更新中、更新失敗。この4つは、使う人にとって全部意味が違うからです。「読み込み中…」が初回もリフレッシュも同じ文言だと、ユーザーは画面が固まったのかと不安になります。

src/features/projects/ProjectListPage.tsx の例です。

import { useState } from "react";
import {
  useProjects,
  type EditableProjectStatus,
  type ProjectFilters,
  type ProjectStatus,
} from "./projects.query";
import { useUpdateProjectStatus } from "./useUpdateProjectStatus";

const defaultFilters: ProjectFilters = {
  status: "all",
  search: "",
  page: 1,
};

function nextStatus(status: EditableProjectStatus): EditableProjectStatus {
  return status === "active" ? "paused" : "active";
}

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : "Unknown error";
}

export function ProjectListPage({
  initialFilters = defaultFilters,
}: {
  initialFilters?: ProjectFilters;
}) {
  const [filters, setFilters] = useState<ProjectFilters>(initialFilters);
  const projectsQuery = useProjects(filters);
  const updateStatus = useUpdateProjectStatus();

  function setStatus(status: ProjectStatus) {
    setFilters((current) => ({ ...current, status, page: 1 }));
  }

  function setSearch(search: string) {
    setFilters((current) => ({ ...current, search, page: 1 }));
  }

  // 初回ロード中。まだ表示するデータが何もない状態
  if (projectsQuery.isPending) {
    return <p role="status">Loading projects...</p>;
  }

  if (projectsQuery.isError) {
    return (
      <section role="alert">
        <h2>Projects could not be loaded</h2>
        <p>{errorMessage(projectsQuery.error)}</p>
        <button type="button" onClick={() => void projectsQuery.refetch()}>
          Retry
        </button>
      </section>
    );
  }

  const projects = projectsQuery.data.items;

  return (
    <section aria-labelledby="projects-title">
      <h2 id="projects-title">Projects</h2>

      <label>
        Search
        <input
          value={filters.search}
          onChange={(event) => setSearch(event.target.value)}
        />
      </label>

      <label>
        Status
        <select
          value={filters.status}
          onChange={(event) => setStatus(event.target.value as ProjectStatus)}
        >
          <option value="all">All</option>
          <option value="active">Active</option>
          <option value="paused">Paused</option>
        </select>
      </label>

      {/* 裏で取り直し中。初回ロードとは別の文言にする */}
      {projectsQuery.isFetching ? (
        <p role="status">Refreshing cached data...</p>
      ) : null}

      {updateStatus.isError ? (
        <p role="alert">Update failed: {errorMessage(updateStatus.error)}</p>
      ) : null}

      <ul>
        {projects.map((project) => {
          const next = nextStatus(project.status);

          return (
            <li key={project.id}>
              <strong>{project.name}</strong>{" "}
              <span aria-label={`status ${project.status}`}>
                {project.status}
              </span>
              <button
                type="button"
                disabled={updateStatus.isPending}
                onClick={() =>
                  updateStatus.mutate({ id: project.id, status: next })
                }
                aria-label={`${next} ${project.name}`}
              >
                Mark {next}
              </button>
            </li>
          );
        })}
      </ul>
    </section>
  );
}

ここでClaude Codeに追いレビューを頼むなら、僕はこの3点を見せます。「初回loadingと裏のfetchingが同じ文言になっていないか」「更新失敗のメッセージが画面に残るか」「ボタンの disabled が広すぎて、別の行の操作まで止めていないか」。最後のやつは地味に多くて、1つ更新中だと全ボタンが押せなくなる、みたいな実装をよく見ます。

SSRとhydrationでkeyをズラさない

Next.jsなどでSSRするときの鉄則は2つあります。1つは、サーバー側で作った QueryClient をリクエスト間で使い回さないこと。使い回すと、別のユーザーのキャッシュが混ざります。リクエストごとに新しいclientを作ります。もう1つは、表示直後の二重fetchを避けるため、staleTime を0より大きくすること。せっかくサーバーで先読みしても、画面が出た瞬間にまた取りに行ったら意味がありません。この挙動は公式のServer Rendering & Hydrationに詳しく書かれています。

import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from "@tanstack/react-query";
import { ProjectListPage } from "@/features/projects/ProjectListPage";
import {
  projectQueries,
  type ProjectFilters,
  type ProjectStatus,
} from "@/features/projects/projects.query";

type PageProps = {
  searchParams?: {
    status?: string;
    search?: string;
    page?: string;
  };
};

function parseStatus(value: string | undefined): ProjectStatus {
  return value === "active" || value === "paused" ? value : "all";
}

export default async function ProjectsPage({ searchParams }: PageProps) {
  const filters: ProjectFilters = {
    status: parseStatus(searchParams?.status),
    search: searchParams?.search ?? "",
    page: Number(searchParams?.page ?? "1") || 1,
  };

  // リクエストごとに新しいclient。ユーザー間でキャッシュを混ぜない
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60_000,
      },
    },
  });

  await queryClient.prefetchQuery(projectQueries.list(filters));

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <ProjectListPage initialFilters={filters} />
    </HydrationBoundary>
  );
}

SSRの典型的な事故は、サーバーとクライアントでquery keyが「ちょっとだけ」違うことです。サーバーでは page: 1(数値)、クライアントでは page: "1"(文字列)。たったこれだけで別のキャッシュ扱いになり、hydration直後にちらついて取り直します。だからこそ、正規化関数とquery key factoryを1か所に置き、Claude Codeには「サーバーページとクライアントhookで同じquery factoryを使って」と必ず明示します。同じ projectQueries.list(filters) を両方から呼ぶ、これが守れていれば事故りません。

MSWでネットワークをモックしてテストする

最後にテストです。TanStack Queryのテストでは、retry を切ると失敗系が速く見えます。切らないと、失敗を確認するテストがリトライを待って無駄に遅くなる。公式のTestingガイドでも、テスト用 QueryClientretry: false を設定する例が出ています。API境界はClaude CodeのMSWモックで固定し、テストの全体方針はClaude Codeのテスト戦略に寄せています。

src/features/projects/ProjectListPage.test.tsx の例です。

import "@testing-library/jest-dom/vitest";
import type { ReactElement } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { ProjectListPage } from "./ProjectListPage";

const server = setupServer(
  http.get("/api/projects", () =>
    HttpResponse.json({
      items: [
        {
          id: "p-1",
          name: "Docs revamp",
          status: "active",
          updatedAt: "2026-06-02T00:00:00.000Z",
        },
      ],
      page: 1,
      hasMore: false,
    }),
  ),
  http.patch("/api/projects/:id", async ({ params, request }) => {
    const body = (await request.json()) as { status: "active" | "paused" };

    return HttpResponse.json({
      id: String(params.id),
      name: "Docs revamp",
      status: body.status,
      updatedAt: "2026-06-02T00:01:00.000Z",
    });
  }),
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

function renderWithClient(ui: ReactElement) {
  // テストごとに新しいclient。retryは切って失敗を速く見る
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
        gcTime: Infinity,
      },
      mutations: {
        retry: false,
      },
    },
  });

  return {
    user: userEvent.setup(),
    ...render(
      <QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>,
    ),
  };
}

describe("ProjectListPage", () => {
  it("loads projects from the API", async () => {
    renderWithClient(<ProjectListPage />);

    expect(screen.getByRole("status")).toHaveTextContent("Loading projects");
    expect(await screen.findByText("Docs revamp")).toBeInTheDocument();
    expect(screen.getByLabelText("status active")).toBeInTheDocument();
  });

  it("rolls back an optimistic update when the mutation fails", async () => {
    // わざとサーバーに500を返させ、楽観的更新が巻き戻るか確認する
    server.use(
      http.patch("/api/projects/:id", () =>
        HttpResponse.json({ message: "boom" }, { status: 500 }),
      ),
    );

    const { user } = renderWithClient(<ProjectListPage />);

    await screen.findByText("Docs revamp");
    await user.click(
      screen.getByRole("button", { name: /paused docs revamp/i }),
    );

    await waitFor(() =>
      expect(screen.getByText(/Update failed:/)).toBeInTheDocument(),
    );
    // active のまま戻っていればロールバック成功
    expect(screen.getByLabelText("status active")).toBeInTheDocument();
  });
});

手元で一気通貫に動かす最小コマンドはこれです。

npm create vite@latest tanstack-query-claude-demo -- --template react-ts
cd tanstack-query-claude-demo
npm install @tanstack/react-query
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event msw
npm pkg set scripts.test="vitest"
npm run test -- --run src/features/projects/ProjectListPage.test.tsx
npm run build

2つ目のテストが大事で、サーバーが500を返したときに楽観的更新がちゃんと巻き戻るかを見ています。ここが通らないと、本番で「成功したように見えて消える」が起きます。

よくある質問

Q. staleTimegcTime の違いは? staleTime は「fresh(新鮮)として扱う時間」で、この間は再取得しません。gcTime は「使われなくなったクエリがキャッシュに残る時間」で、過ぎると破棄されます。デフォルトではデータは取得直後からstale扱い、非アクティブなクエリは約5分でガベージコレクトされます。再取得の頻度を決めたいなら staleTime、メモリの持ち方を決めたいなら gcTime です。

Q. v5で cacheTime が使えないのはなぜ? v5で cacheTimegcTime にリネームされました。意味は同じ(非アクティブなクエリの保持時間)ですが、名前が「キャッシュ全体の時間」と誤解されやすかったための変更です。古い記事のコピペでは cacheTime が残っていることが多いので、Claude Codeへの依頼文で明示的に禁止しておくと安全です。

Q. mutation成功後に invalidateQueries() を引数なしで呼んでいい? やめたほうがいいです。引数なしは全キャッシュを無効化するので、動いているようには見えますが、画面が増えるほど余計なネットワークリクエストが増えます。projectKeys.lists() のようにprefixで対象を絞るか、exact で1件だけを狙います。

Q. query keyにDateやMapを入れてもいい? 入れないでください。query keyは JSON.stringify できる必要があります。Dateはそのまま入れず、ISO文字列などに変換してから入れます。関数やclassインスタンスも同じ理由でNGです。

Q. SSRで「画面は速いのに二重fetchする」のはなぜ? 原因はほぼ2つです。staleTime が0で、表示直後にfreshでないと判断されている。または、サーバーとクライアントでquery keyの型が微妙に違う(page: 1page: "1" など)。共通のquery key factoryを両方から呼び、staleTime を0より大きくすれば直ります。

実際に試した結果

このやり方を何本かのリポジトリで回してみて、いちばん手戻りが減ったのは「query key factoryを最初に固定する」一点でした。

以前の僕は、UIから作っていました。画面が動くと安心するからです。でもその順番だと、検索条件の正規化漏れ、SSRでのkey不一致、編集後の一覧更新漏れが、決まって後から見つかる。発見が遅いぶん、直すのも面倒でした。

順番を逆にして、query key・staleTimegcTime・mutationのロールバック・MSWテストを最初の依頼に全部入れたら、Claude Codeが出す差分は小さくなり、レビューで見るのは「どのキャッシュを意図的に触ったか」だけになりました。動くものを早く出すより、住所を先に決める。遠回りに見えて、これがいちばん速い、というのが今の結論です。

繰り返し使う依頼文やレビュー観点を整えたいなら、まずは無料チートシートから。チームの型として固めるなら教材・テンプレート、実リポジトリで権限やレビュー、運用まで設計したいならClaude Code研修・導入相談が次の一歩です。

#Claude Code #TanStack Query #React #キャッシュ #楽観的更新
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。