# Pathrule Pattern: TanStack Query (1.0.0)
# ::pathrule:package:tanstack-query

### [RULE] Never fetch server state in useEffect  (path: /src)
<!-- scope: folder | priority: high | strict -->

Server 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.

- Replace any `useEffect` that calls `fetch` or an API client with `useQuery` or `useSuspenseQuery`.
- 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`.)
- 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.
- For dependent queries, gate with the `enabled` option or compose `useSuspenseQuery` components; never gate a fetch inside an effect.
- Reserve `useEffect` for true side effects such as subscriptions and DOM work.

---

### [RULE] Invalidate related queries after every mutation  (path: /src)
<!-- scope: folder | priority: high | strict -->

A mutation that changes server data must tell the cache, or the UI keeps rendering stale data until some unrelated refetch trigger happens to fire. `invalidateQueries` marks matching entries stale and refetches the active ones in the background, overriding any `staleTime`.

- In `onSuccess` (or `onSettled` when you want it to run after errors too), call `queryClient.invalidateQueries({ queryKey: keys.lists() })` for every list or detail the write touches.
- Pass key-factory references, not inline arrays, so invalidation targets exactly the keys that were set. `invalidateQueries` matches by prefix, so `keys.all` clears an entire entity.
- For optimistic UI: `onMutate` runs `cancelQueries`, snapshots with `getQueryData`, and writes with `setQueryData`; `onError` rolls the snapshot back; `onSettled` still invalidates to reconcile with the server.
- Do not lean on `setQueryData` instead of invalidation unless the mutation returns the exact server shape for that key; otherwise the next background refetch silently overwrites your guess.

---

### [MEMORY] Query key factories are the cache contract  (path: /src/api)

Query 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.

- Shape each factory hierarchically so a single call can clear a whole entity:

```ts
export const todoKeys = {
  all: ['todos'] as const,
  lists: () => [...todoKeys.all, 'list'] as const,
  list: (filters: TodoFilters) => [...todoKeys.lists(), filters] as const,
  details: () => [...todoKeys.all, 'detail'] as const,
  detail: (id: string) => [...todoKeys.details(), id] as const,
}
```

- 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.
- 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.
- 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']`).

---

### [MEMORY] TanStack Query v5 defaults you must override  (path: /src)

TanStack 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.

- `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`.
- `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.
- `retry` defaults to `3` with exponential backoff; turn it down or off for mutations and for endpoints where retrying is pointless (4xx).
- `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.
- 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).

---

### [MEMORY] Next.js App Router QueryClient setup and prefetch  (path: /app)

The 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.

- 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):

```ts
function makeQueryClient() {
  return new QueryClient({
    defaultOptions: { queries: { staleTime: 60 * 1000 } },
  })
}
let browserClient: QueryClient | undefined
export function getQueryClient() {
  if (typeof window === 'undefined') return makeQueryClient()
  return (browserClient ??= makeQueryClient())
}
```

- 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.
- 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.
- 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.

---

### [SKILL] tanstack-query-review  (path: /)

---
name: tanstack-query-review
description: 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.
---

# TanStack Query review

- [ ] No server data is fetched in `useEffect` plus `useState`; every read uses `useQuery` or `useSuspenseQuery`
- [ ] `query.data` is read directly in render, never copied into `useState` or a global store
- [ ] Query keys come from a centralized factory and include every input the query depends on (filters, ids, pagination)
- [ ] Shared queries are defined with `queryOptions()` and reused across hooks, `prefetchQuery`, and `setQueryData`
- [ ] `staleTime` is set deliberately rather than left at the `0ms` default; `gcTime`, `retry`, and `staleTime: Infinity` vs `'static'` are used with intent
- [ ] Every `useMutation` invalidates or updates the queries its write affects in `onSuccess` or `onSettled`, using key-factory references
- [ ] Optimistic updates cancel in-flight queries, snapshot previous data, roll back in `onError`, and reconcile in `onSettled`
- [ ] Loading and error states read from `isPending` and `isError` (or Suspense + Error Boundary), not hand-rolled flags
- [ ] 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`
- [ ] No deprecated v5 APIs: no `useQuery({ suspense })`, no `cacheTime`, no `keepPreviousData` (use `placeholderData: keepPreviousData`)
