# TanStack Query (React Query v5) — Claude Code rules You are working with **TanStack Query v5** (the `@tanstack/react-query` package). Follow the conventions below; they mirror the official docs. ## Setup & syntax - v5 uses a single **object signature** everywhere: `useQuery({ queryKey, queryFn })`, `useMutation({ mutationFn })`. Don't use the old positional/overload syntax. - A query needs two things: a **unique key** and a **function that returns a promise** which either resolves the data or **throws** an error. - The `queryFn` must throw (or return a rejected promise) on failure so the query enters the error state. `fetch` does **not** throw on `4xx/5xx` — check `res.ok` and throw yourself. ## Query keys - Query keys are **always arrays** at the top level and must be serializable with `JSON.stringify` and unique to the query's data. - Simple keys for list/index resources: `['todos']`. Hierarchical / parameterized resources add serializable items: `['todo', 5, { preview: true }]`. - **Include every variable the `queryFn` depends on in the key.** Each key is cached independently, and changing a variable refetches automatically. The key is your dependency array — don't fetch off stale closures. - Object properties in a key are hashed **deterministically**, so `{ status, page }` and `{ page, status }` are equal. **Array item order matters**: `['todos', status, page]` differs from `['todos', page, status]`. ## Reading query state - `status` describes the **data**: `pending` (no data yet), `error`, `success`. Use the boolean shortcuts `isPending` / `isError` / `isSuccess`. - `fetchStatus` describes the **`queryFn`**: `fetching`, `paused` (wanted to fetch but no network), `idle`. `isFetching` is true during background refetches too. - Status answers "do we have data?"; fetchStatus answers "is the function running?". Handle them separately — a `success` query can also be `fetching` in the background. ## Defaults — know them before overriding - `staleTime` defaults to **`0`**: data is considered stale immediately. Stale queries refetch automatically on new mounts, window refocus, and network reconnect. Raise `staleTime` to reduce refetching; that is the right knob, not disabling refetch flags everywhere. - `gcTime` defaults to **5 minutes** (`1000 * 60 * 5`): once a query has no active observers it becomes **inactive** and is garbage-collected after this window. `staleTime` (freshness) and `gcTime` (cache retention) are independent — don't conflate them. - Failed queries are **retried 3 times with exponential backoff** by default. Don't blindly set `retry: false`; if you must, scope it (e.g. don't retry `404`s) rather than killing retries globally. - Results are **structurally shared** so unchanged data keeps the same reference. Don't fight this with manual memoization of `data`. ## Mutations & invalidation - Use `useMutation({ mutationFn })` for create/update/delete and other server side-effects. Trigger it by calling `mutate(variables)`. - After a successful mutation, **invalidate the affected queries** so the UI reflects the new server state: call `queryClient.invalidateQueries({ queryKey: [...] })` (typically in `onSuccess` / `onSettled`), or write the cache directly with `setQueryData`. - Lifecycle callbacks run in order: `onMutate` → `onSuccess` / `onError` → `onSettled`. Use `mutateAsync` only when you need a promise to await/throw at the call site. ## Don't - Don't use `useQuery` for things that aren't **server state** (form inputs, UI toggles, derived values) — keep those in `useState` / derive on render. - Don't omit variables from the query key, then read them via closure — the cache will serve the wrong data. - Don't disable retries or refetch-on-focus reflexively; reach for `staleTime` first and understand the default you're turning off. - Don't swallow errors in the `queryFn` (returning `undefined` on failure) — throw so the query reports `error`.