TanStack Query

Pathrule2 Rules • 3 Memories • 1 Skill

A guardrail bundle for TanStack Query v5 in React apps. It enforces structured query key factories, deliberate staleTime, and mutation-driven invalidation so server state stays consistent. The rules keep data fetching out of effects and push every query through reusable queryOptions.

Suggested path map

Pathrule places each piece on the matching path, so your assistant only sees it where it belongs. This is the scoping you get on import; you can adjust it in your workspace.

/ workspace root
tanstack-query-review
src/
Never fetch server state in useEffect
Invalidate related queries after every mutation
TanStack Query v5 defaults you must override
api/
Query key factories are the cache contract
app/
Next.js App Router QueryClient setup and prefetch

Rules

2
Never fetch server state in useEffect/srchighstrictUse useQuery or useSuspenseQuery for any server data, never useEffect plus useState.
1Server state belongs to TanStack Query, not to component effects. Fetching in `useEffect` and storing the result in `useState` reintroduces every problem the library exists to solve: no caching, no request dedupe, manual loading and error flags, and race conditions when the user navigates faster than the request resolves.
2 
3- Replace any `useEffect` that calls `fetch` or an API client with `useQuery` or `useSuspenseQuery`.
4- Read status from `query.status`, `isPending`, and `isError`, not from hand-rolled `useState` flags. (`isLoading` in v5 means `isPending && isFetching`; the initial-load flag is `isPending`.)
5- Reference `query.data` directly in render. Do not copy it into `useState` or a global store, or background refetches will no longer reflect in the UI.
6- For dependent queries, gate with the `enabled` option or compose `useSuspenseQuery` components; never gate a fetch inside an effect.
7- Reserve `useEffect` for true side effects such as subscriptions and DOM work.

Memories

3
Query key factories are the cache contract/src/apiCentralize hierarchical query keys per entity so invalidation is predictable.
1Query keys are how TanStack Query identifies, dedupes, and invalidates cache entries, so they live in one factory per entity instead of as scattered inline arrays.
2 
3- Shape each factory hierarchically so a single call can clear a whole entity:
4 
5```ts
6export const todoKeys = {
7 all: ['todos'] as const,
8 lists: () => [...todoKeys.all, 'list'] as const,
9 list: (filters: TodoFilters) => [...todoKeys.lists(), filters] as const,
10 details: () => [...todoKeys.all, 'detail'] as const,
11 detail: (id: string) => [...todoKeys.details(), id] as const,
12}
13```
14 
15- Include EVERY input the query depends on (filters, ids, pagination) in the key. The key is the dependency array: changing it is how a new fetch is triggered, and omitting an input serves stale data for the wrong arguments.
16- Pair each factory with `queryOptions()` so the same key, `queryFn`, and `staleTime` are reused verbatim across `useQuery`, `useSuspenseQuery`, `prefetchQuery`, and `setQueryData`. This also makes `queryClient.getQueryData(todoKeys.detail(id))` fully typed.
17- Keys must be JSON-serializable; objects are compared by deep structure, not reference, but key order within an array matters (`['todos', 'list']` differs from `['list', 'todos']`).
TanStack Query v5 defaults you must override/srcKnow the v5 freshness defaults before tuning refetch and cache behavior.
1TanStack Query v5 (`@tanstack/react-query` 5.x) ships aggressive freshness defaults that surprise teams expecting cache-first behavior. Set them deliberately per query rather than fighting symptoms.
2 
3- `staleTime` defaults to `0`, so data is stale the instant it arrives and refetches on mount, window focus, and reconnect. For data that does not change every second, set a realistic value such as `staleTime: 5 * 60 * 1000`.
4- `gcTime` (renamed from `cacheTime` in v5) defaults to 5 minutes and controls when INACTIVE cache entries are garbage collected. It is not staleness; a fresh-but-inactive query can still be collected.
5- `retry` defaults to `3` with exponential backoff; turn it down or off for mutations and for endpoints where retrying is pointless (4xx).
6- `staleTime: Infinity` keeps data fresh forever but still honors `invalidateQueries`; use it for data that only changes through your own mutations. `staleTime: 'static'` (v5) never refetches at all, even on manual invalidation, for truly immutable data like build-time feature flags.
7- v5 removed `useQuery({ suspense: true })`. Use the dedicated `useSuspenseQuery`; it has no `enabled` option because loading is handled by Suspense and errors by an Error Boundary. `keepPreviousData` is also gone, replaced by `placeholderData: keepPreviousData` (imported from the package).
Next.js App Router QueryClient setup and prefetch/appOne QueryClient per request on the server, a browser singleton on the client, then hydrate.
1The most damaging real-world TanStack Query bug in Next.js is a module-level QueryClient: on the server it is shared across all requests, so one user's cached data leaks to the next. Use a request-scoped factory.
2 
3- Create the client through a helper that returns a fresh instance on the server and a memoized singleton in the browser (the browser branch prevents a new client when React suspends mid-render):
4 
5```ts
6function makeQueryClient() {
7 return new QueryClient({
8 defaultOptions: { queries: { staleTime: 60 * 1000 } },
9 })
10}
11let browserClient: QueryClient | undefined
12export function getQueryClient() {
13 if (typeof window === 'undefined') return makeQueryClient()
14 return (browserClient ??= makeQueryClient())
15}
16```
17 
18- Set a default `staleTime` above `0` (60s is the documented baseline). Without it the client immediately refetches everything you just rendered on the server, defeating SSR.
19- In a Server Component, prefetch with `getQueryClient()` + `await queryClient.prefetchQuery(todoQueryOptions(id))`, then render `<HydrationBoundary state={dehydrate(queryClient)}>`. Child Client Components call `useQuery(todoQueryOptions(id))` with the same `queryOptions` and read from the hydrated cache.
20- For streaming, do not `await` the prefetch and enable pending-query dehydration (v5.40+): set `dehydrate.shouldDehydrateQuery` to `(q) => defaultShouldDehydrateQuery(q) || q.state.status === 'pending'`. Keep `'use client'` and the `QueryClientProvider` in a single shared providers component.

Skills

1
tanstack-query-review/rootPre-merge checklist for TanStack Query v5 usage.
1---
2name: tanstack-query-review
3description: Review checklist for TanStack Query v5 code. Use when adding or changing useQuery, useSuspenseQuery, useMutation, query keys, queryOptions, QueryClient config, or Next.js SSR hydration in a React or Next.js codebase.
4---
5 
6# TanStack Query review
7 
8- [ ] No server data is fetched in `useEffect` plus `useState`; every read uses `useQuery` or `useSuspenseQuery`
9- [ ] `query.data` is read directly in render, never copied into `useState` or a global store
10- [ ] Query keys come from a centralized factory and include every input the query depends on (filters, ids, pagination)
11- [ ] Shared queries are defined with `queryOptions()` and reused across hooks, `prefetchQuery`, and `setQueryData`
12- [ ] `staleTime` is set deliberately rather than left at the `0ms` default; `gcTime`, `retry`, and `staleTime: Infinity` vs `'static'` are used with intent
13- [ ] Every `useMutation` invalidates or updates the queries its write affects in `onSuccess` or `onSettled`, using key-factory references
14- [ ] Optimistic updates cancel in-flight queries, snapshot previous data, roll back in `onError`, and reconcile in `onSettled`
15- [ ] Loading and error states read from `isPending` and `isError` (or Suspense + Error Boundary), not hand-rolled flags
16- [ ] In Next.js, the server uses a request-scoped QueryClient (no module-level instance) with a default `staleTime > 0`, and prefetched data is passed through `HydrationBoundary` + `dehydrate`
17- [ ] No deprecated v5 APIs: no `useQuery({ suspense })`, no `cacheTime`, no `keepPreviousData` (use `placeholderData: keepPreviousData`)

Why this pattern

Server state drifts out of sync because queries use ad-hoc keys, fetch inside effects, and never invalidate after mutations.

Built for React and Next.js teams using TanStack Query v5 for server state.

Keeps your assistant from:

  • Fetching data inside useEffect and storing it in component state instead of using useQuery
  • Ad-hoc string query keys that make invalidation unpredictable and silently serve stale data
  • Mutations that update the server but never invalidate or update the related query cache
License
Apache-2.0
Version
1.0.0
Updated
2026-06-09
View source