Use Cases (अपडेट: 2/6/2026)

Claude Code और TanStack Query: React data fetching और cache की practical guide

Claude Code से TanStack Query सुरक्षित लागू करें: query keys, staleTime, optimistic update, SSR hydration और tests.

Claude Code और TanStack Query: React data fetching और cache की practical guide

अगर आप Claude Code से सिर्फ “TanStack Query जोड़ दो” कहते हैं, तो अक्सर code दिखने में सही होता है लेकिन cache behavior कमजोर रहता है। query keys हर file में अलग shape लेती हैं, staleTime बिना कारण का number बन जाता है, mutation के बाद कौन-सी list refresh करनी है यह साफ नहीं रहता, और SSR hydration के तुरंत बाद client दोबारा fetch कर सकता है।

यह guide एक project list screen के आधार पर TanStack Query v5 को सुरक्षित तरीके से implement करना दिखाती है: query key design, staleTime और gcTime, mutation invalidation, optimistic updates, loading/error states, SSR hydration और mocked network tests. API facts के लिए official TanStack docs देखें: Query Keys, Important Defaults, Query Invalidation, Optimistic Updates, Server Rendering & Hydration, और Testing.

React boundaries के लिए Claude Code React development, tests के लिए Claude Code testing strategies, API mock के लिए Claude Code MSW mock, और client state अलग रखने के लिए Claude Code Zustand साथ में पढ़ें।

पहले boundary तय करें

TanStack Query server state संभालता है। server state वह data है जिसकी source of truth API, database या backend service में होती है। Search input में अभी लिखा जा रहा text, modal open है या नहीं, selected tab कौन-सा है, ये temporary UI state हैं।

Claude Code को edit करने से पहले यह contract दें:

DecisionExampleReview में फायदा
query key unit["projects", "list", filters]cache address stable रहता है
freshnesslist 60 seconds, detail 5 minutesrefetch behavior explain होता है
mutation के बादlist prefix और detail key invalidateपुराना UI नहीं बचता
optimistic updatestatus तुरंत बदलता दिखेfailure में rollback हो सके
verificationVitest, MSW, buildreview evidence-based होता है

query key को cache address समझें। TanStack Query v5 में query key top-level array होनी चाहिए, serializable होनी चाहिए और fetched data को uniquely describe करना चाहिए। इसलिए ["projects", "list", { status: "active" }] component name नहीं, data conditions बताती है।

Practical use cases

पहला use case admin list है: users, invoices, tickets, projects या products. Search, status और page query key में जाएं। Claude Code से साफ कहें कि filters normalize करे, ताकि Billing और billing अलग cache न बनें।

दूसरा use case detail और edit flow है। Detail query ["projects", "detail", id] हो सकती है। Edit success के बाद detail key और list prefix दोनों invalidate करें। invalidateQueries matching queries को stale mark करता है और rendered query को background में refetch करता है।

तीसरा use case fast toggle है: status, favorite, archive या assignee. यहां optimistic update अच्छा है, लेकिन सिर्फ UI पहले बदलना काफी नहीं। In-flight refetch cancel करें, old cache snapshot लें, error पर rollback करें, और अंत में invalidate करें।

चौथा use case SSR है। Server prefetch और hydration first render तेज कर सकते हैं, पर server और client query key अलग हुई तो cache miss होगा। SSR में same query factory use करें और सामान्यतः positive staleTime रखें।

query keys, staleTime और gcTime

Claude Code prompt:

Existing React + TypeScript project पढ़कर TanStack Query v5 से projects list implement करें।
Top-level array query keys, normalized filters, list staleTime 60 seconds, gcTime 10 minutes use करें।
Pagination के लिए queryOptions और placeholderData: keepPreviousData use करें।
API functions, types, query key factory और hook एक module में रखें।
पुराना cacheTime option name use न करें।

इस code को src/features/projects/projects.query.ts में रखें। staleTime data को fresh मानने की अवधि है। gcTime inactive query data cache में कितनी देर रहेगा, यह तय करता है। v5 में gcTime use करें।

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;
};

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);
}

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,
      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,
  });
}

यहां key factory सबसे महत्वपूर्ण हिस्सा है। हर जगह staleTime: Infinity लगाना safe default नहीं है, क्योंकि team dashboard में data किसी और user से भी बदल सकता है।

mutation invalidation और optimistic update

Mutation server write है। Claude Code से success path के साथ failure path भी बनवाएं। Safe flow है: affected queries cancel, cache snapshot, optimistic write, error में rollback, और settled पर targeted invalidation.

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) => {
      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),
      );

      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) }),
      ]);
    },
  });
}

Common pitfalls: detail cache update करके visible lists भूल जाना, या बिना filter के invalidateQueries() call करना। Claude Code से PR summary में invalidated keys list करवाएं।

loading/error states

Initial loading, background refresh, mutation pending और mutation error अलग states हैं। UI में इन्हें अलग दिखाएं।

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>
  );
}

Review prompt: “initial loading, background refresh, mutation failure, retry और disabled behavior अलग-अलग दिख रहे हैं या नहीं, जांचें.”

SSR और hydration

SSR में हर request के लिए नया QueryClient बनाएं और server/client दोनों में same query factory use करें।

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,
  };

  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60_000,
      },
    },
  });

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

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

Hidden bug type drift है: server पर page: "1" और client पर page: 1 अलग keys बनाते हैं। Parser और key factory central रखें।

MSW के साथ mocked network tests

Component tests को real API से न जोड़ें। MSW से HTTP mock करें, हर test में नया QueryClient बनाएं और retry: false रखें।

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) {
  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 () => {
    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(),
    );
    expect(screen.getByLabelText("status active")).toBeInTheDocument();
  });
});

Verification commands:

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

Article check:

node scripts/check-code-fences.mjs
node scripts/check-updated-article-quality.mjs

Failure modes और safe prompt

Query key में functions, class instances या raw Date न डालें। Date को stable string में बदलें। staleTime और gcTime अलग हैं: पहला freshness/refetch को प्रभावित करता है, दूसरा inactive queries की cache lifetime को। Default में data stale माना जाता है और inactive queries करीब 5 minutes में garbage collect होती हैं।

बिना filter के global invalidation न करें। queryClient.invalidateQueries() small app में आसान लगता है, पर बड़े app में unnecessary requests बनाता है। SSR और client के लिए अलग key factory भी न बनाएं।

केवल TanStack Query v5 use करें। gcTime use करें, cacheTime नहीं।
query key factory, API functions, hooks, UI और tests अलग रखें।
mutation में cancel, snapshot, optimistic update, rollback और targeted invalidation implement करें।
SSR prefetch key और client hook key identical हैं या नहीं review करें।
Verification commands दें: npm run test -- --run ProjectListPage.test.tsx और npm run build।

CTA और practical result

TanStack Query Claude Code workflow standardize करने के लिए अच्छा area है, क्योंकि cache bugs शांत दिखते हैं लेकिन महंगे होते हैं। शुरुआत के लिए free Claude Code cheatsheet देखें। Reusable prompts और templates के लिए ClaudeCodeLab products उपयोगी हैं। Team repository में query key rules, permissions, review policy और rollout training लागू करनी हो तो Claude Code training and consultation बेहतर अगला कदम है।

Masa ने इस workflow को test किया तो सबसे बड़ा सुधार query key factory पहले तय करने से आया। UI पहले बनाने पर filter normalization छूट गया, SSR keys अलग बन गईं, और mutation के बाद list पुरानी रही। जब first prompt में query keys, staleTime, gcTime, rollback और MSW tests शामिल किए, Claude Code diff छोटा हुआ और review सीधे इस सवाल पर आया: कौन-सा cache बदला और क्यों।

#Claude Code #TanStack Query #React #data fetching #cache
मुफ़्त

मुफ़्त PDF: Claude Code cheatsheet

Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.

हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.

Masa

लेखक के बारे में

Masa

Claude Code workflow और team adoption पर काम करने वाला engineer.