43 lines | 3.9 KB

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 404s) 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: onMutateonSuccess / onErroronSettled. 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.