# TanStack Query

You are working in a React 19 project with TypeScript and TanStack Query v5 (`@tanstack/react-query`). Write code for the version in `package.json`: v5 renamed or removed options that most older examples still use.

## Before you write code

- Check the `@tanstack/react-query` major version (these rules are v5), whether `@tanstack/react-query-devtools` and `@tanstack/eslint-plugin-query` are installed, and where the project creates its `QueryClient` and keeps its query options. Extend those; don't add a second provider.
- In a Next.js App Router project, read `node_modules/next/dist/docs/01-app/02-guides/client-side-data-fetching/` (`index.md` and `tanstack-query.md`): providers, hydration and Cache Components for the installed Next.js.

## When to use it

- Use it for server data that Client Components share, refetch, paginate, poll, update optimistically or load after an interaction (search as you type, infinite lists, live dashboards).
- Don't use it for data a Server Component can read once and render: await it on the server. When a Client Component needs server data once, pass it a promise from the Server Component and read it with `use()`.
- The query cache owns server data: never copy it into `useState`, context or a global store, and never fetch in `useEffect`.

## Query options and keys

Define each query once with `queryOptions` (or `infiniteQueryOptions`) in a module the server and the client can both import, and reuse that object everywhere:

```ts
import { queryOptions } from "@tanstack/react-query";

export const todoKeys = {
  all: ["todos"] as const,
  list: (filters: TodoFilters) => [...todoKeys.all, "list", filters] as const,
  detail: (id: string) => [...todoKeys.all, "detail", id] as const,
};

export const todoQuery = (id: string) =>
  queryOptions({
    queryKey: todoKeys.detail(id),
    queryFn: async ({ signal }): Promise<Todo> => {
      const res = await fetch(`/api/todos/${id}`, { signal });
      if (!res.ok) throw new Error(`Todo ${id}: ${res.status}`);
      return res.json();
    },
  });
```

- Keys are arrays from general to specific and hold every input the query function reads: ids, filters, page, sort, and the user or tenant when the response depends on it. Object order inside a key doesn't matter.
- The same options go to `useQuery`, `useSuspenseQuery`, `prefetchQuery`, `ensureQueryData` and `getQueryData(todoQuery(id).queryKey)`, which is then typed.
- `fetch` doesn't reject on 4xx or 5xx: check `res.ok` and throw. Pass `signal` so cancelled queries abort. Validate untrusted responses with a schema.

## Reading data

- `useQuery` when the component renders its own states. `isPending` means no data yet; `isFetching` means a request is in flight, including background refetches. Keep data on screen during a refetch and show a quiet indicator.
- `useSuspenseQuery` when a `<Suspense>` boundary owns loading and an error boundary owns errors: `data` is always defined, and `enabled` and `placeholderData` don't exist there. Several suspense queries in one component run one after another: split them into siblings or use `useSuspenseQueries`.
- `staleTime` (default 0) decides when data refetches on mount, window focus and reconnect. Set a default on the `QueryClient` (such as 60 seconds) and override it per query. `gcTime` (default 5 minutes) is how long unused data stays in memory, not freshness.
- `select` derives what one component needs from the cached response; keep the function stable (module scope) when it is expensive.
- `placeholderData` is shown but never cached: use it for a preview, such as the row from a list. `initialData` is written to the cache and counts as fresh for `staleTime`: use it only for complete data.
- Polling: `refetchInterval` (milliseconds, or a function of the query that returns `false` to stop). It pauses in background tabs unless `refetchIntervalInBackground` is set.
- Prefetch on intent: `queryClient.prefetchQuery(todoQuery(id))` on a link's hover or focus.
- Dependent queries: `queryFn: userId ? () => fetchUser(userId) : skipToken` (typed), or `enabled: Boolean(userId)`. Separate `useQuery` calls already run in parallel; for a dynamic list use `useQueries({ queries: ids.map((id) => todoQuery(id)) })` with `combine`.
- Pages: `placeholderData: keepPreviousData` keeps the last page on screen; use `isPlaceholderData` to disable Next and dim the list, and `prefetchQuery` the next page.
- Infinite lists: `useInfiniteQuery` with `initialPageParam` (required) and `getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined` (`undefined` or `null` ends the list). Render `data.pages.flatMap(…)`, call `fetchNextPage()` only when `hasNextPage && !isFetchingNextPage`, and cap long feeds with `maxPages`.

## Mutations

- `useMutation({ mutationFn, onSuccess, onError, onSettled })`; its pending flag is `isPending`. Prefer `mutate` with callbacks; `mutateAsync` only where you await it and catch.
- After a write, invalidate what changed: `queryClient.invalidateQueries({ queryKey: todoKeys.all })` matches by prefix. Return that promise from `onSettled` so the mutation stays pending until fresh data arrives. When the response is the new entity, `setQueryData` it instead.
- Simple optimistic UI: render `variables` while `isPending` (in another component, read them with `useMutationState` and a `mutationKey`). Nothing to roll back.
- Optimistic cache update with rollback:

```ts
const queryClient = useQueryClient();
const save = useMutation({
  mutationFn: updateTodo,
  onMutate: async (next: Todo) => {
    const { queryKey } = todoQuery(next.id);
    await queryClient.cancelQueries({ queryKey });
    const previous = queryClient.getQueryData(queryKey);
    queryClient.setQueryData(queryKey, next);
    return { previous };
  },
  onError: (_error, next, snapshot) => {
    queryClient.setQueryData(todoQuery(next.id).queryKey, snapshot?.previous);
  },
  onSettled: (_data, _error, next) =>
    queryClient.invalidateQueries({ queryKey: todoQuery(next.id).queryKey }),
});
```

- Update cache entries immutably. A `mutationFn` can call a Server Function; when nothing on screen reads that data from the cache, a plain form action with `useActionState` is simpler.

## Errors and retries

- Queries retry 3 times with backoff in the browser and not at all on the server. Don't retry client errors (here `HttpError` is your fetch wrapper's error): `retry: (count, error) => count < 3 && !(error instanceof HttpError && error.status < 500)`.
- `useQuery` has no `onSuccess`, `onError` or `onSettled` in v5. For app-wide error toasts, pass `queryCache: new QueryCache({ onError })` to the `QueryClient`; for local errors, render `error`.
- `throwOnError` sends errors to the nearest error boundary. Before retrying from that boundary, reset queries with `QueryErrorResetBoundary` or `useQueryErrorResetBoundary().reset`, or the cached error is thrown again.
- When your errors aren't `Error`, register the type once: `declare module "@tanstack/react-query" { interface Register { defaultError: HttpError } }`.

## Server rendering (Next.js App Router)

- One `QueryClient` per server request and one per browser tab. Never share a module-level client on the server (users would see each other's data), and never `new QueryClient()` in a component body.

```tsx
"use client";

import { isServer, QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";

const makeClient = () =>
  new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 } } });
let browserClient: QueryClient | undefined;
const getClient = () => (isServer ? makeClient() : (browserClient ??= makeClient()));

export function QueryProvider({ children }: { children: ReactNode }) {
  return <QueryClientProvider client={getClient()}>{children}</QueryClientProvider>;
}
```

- Render the provider in the nearest layout that needs it. To prefetch, create a `QueryClient` in the Server Component (shared across one request's components with `cache(() => new QueryClient())`), call `prefetchQuery({ ...todoQuery(id), queryFn: () => getTodo(id) })` (on the server call the data layer directly: a relative URL only resolves in the browser), and wrap the Client Component in `<HydrationBoundary state={dehydrate(queryClient)}>`. The client reads the same options. Keep `staleTime` above 0 so hydrated data isn't refetched at once.
- To stream instead of block, don't await `prefetchQuery`, and dehydrate pending queries with `shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === "pending"` inside a `<Suspense>` boundary.
- `prefetchQuery` never throws; use `fetchQuery` when a failure should become `notFound()` or the error page.
- With `cacheComponents` on, keep queries the first render needs inside `<Suspense>`. `dehydrate()` reads the current time, which fails while prerendering cached segments: use the guide's prerenderable hydration helper. Keep each query key and its cache tag in one shared module; after a write through a Server Function, call `updateTag(tag)` there and `invalidateQueries` on the client.

## Devtools and lint

- Render `ReactQueryDevtools` from `@tanstack/react-query-devtools` inside the provider; it is left out of production builds by default.
- With `@tanstack/eslint-plugin-query` installed, keep its recommended rules on (`exhaustive-deps` for keys, `stable-query-client`).

## Do not

- Do not write v4: `cacheTime` (now `gcTime`), `keepPreviousData: true`, `useErrorBoundary` (now `throwOnError`), `isLoading` to mean "no data yet" (now `isPending`), `onSuccess` on queries, positional `useQuery(key, fn)` or `invalidateQueries(key)`, or imports from `react-query`.
- Do not pass new arguments to `refetch()`: change the key.
- Do not build keys by string concatenation, or leave out a variable the query function uses.
- Do not use `useQuery` for writes or `useMutation` for reads.

## Before you finish

- Run the type checker and the linter.
- In the devtools, check every key, that `staleTime` stops refetch loops, and that each mutation invalidates the right prefix.
- Throttle the network and check loading, error, retry, empty and background-refetch states; force a failed mutation and watch it roll back.
- With server rendering: no refetch right after hydration, no hydration warnings, and no client shared between requests.
