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
queryFnmust throw (or return a rejected promise) on failure so the query enters the error state.fetchdoes not throw on4xx/5xx— checkres.okand throw yourself.
Query keys
- Query keys are always arrays at the top level and must be serializable with
JSON.stringifyand 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
queryFndepends 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
statusdescribes the data:pending(no data yet),error,success. Use the boolean shortcutsisPending/isError/isSuccess.fetchStatusdescribes thequeryFn:fetching,paused(wanted to fetch but no network),idle.isFetchingis true during background refetches too.- Status answers "do we have data?"; fetchStatus answers "is the function running?". Handle them separately — a
successquery can also befetchingin the background.
Defaults — know them before overriding
staleTimedefaults to0: data is considered stale immediately. Stale queries refetch automatically on new mounts, window refocus, and network reconnect. RaisestaleTimeto reduce refetching; that is the right knob, not disabling refetch flags everywhere.gcTimedefaults 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) andgcTime(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 retry404s) 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 callingmutate(variables). - After a successful mutation, invalidate the affected queries so the UI reflects the new server state: call
queryClient.invalidateQueries({ queryKey: [...] })(typically inonSuccess/onSettled), or write the cache directly withsetQueryData. - Lifecycle callbacks run in order:
onMutate→onSuccess/onError→onSettled. UsemutateAsynconly when you need a promise to await/throw at the call site.
Don't
- Don't use
useQueryfor things that aren't server state (form inputs, UI toggles, derived values) — keep those inuseState/ 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
staleTimefirst and understand the default you're turning off. - Don't swallow errors in the
queryFn(returningundefinedon failure) — throw so the query reportserror.