Next.js + TanStack Query: reads and writes
The canonical read/write data pattern for a Next.js App Router app: reads flow server prefetch → hydration → client useQuery; writes are a server action that throws, triggered client-side with useTransition or optimistic useMutation. Fetching functions live in lib/data/*.ts, mutation functions in lib/action/*.ts — see next-monorepo-pattern for the full app file-layout convention these fit into.
Fetching — server prefetch → hydration → client useQuery
Reads flow server component prefetch → TanStack Query hydration → client useQuery on the identical key. Fetching functions always use "use cache" + cacheTag + cacheLife (cache components is a required next.config.ts setting for this convention — see references/fetching.md if it isn't enabled yet), and never read cookies/headers/session directly.
Full pattern, rules, and gotchas (typed API client call syntax, parallel queries, keepPreviousData for filtered tables, cache invalidation): references/fetching.md.
Worked files: fetching.ts, page.tsx, table.tsx.
Mutations — server action throws → toast
Writes are a server action that throws on failure (the thrown message becomes the toast), triggered from the client with useTransition (simple) or useMutation (optimistic, for cached lists), forms via TanStack Form + Zod. Mutation functions call updateTag with the same tag the corresponding lib/data fetch uses.
Full pattern, rules, and variants (optimistic useMutation, the three list-write shapes, toast conventions, destructive actions): references/mutations.md.
Worked files: mutation.ts, create-form.tsx, create-dialog.tsx.
Common mistakes
- Calling the typed API client directly from a client component instead of through a
lib/data/lib/actionfunction +useQuery/useMutation. → breaks hydration and cache invalidation. - Awaiting independent fetches in a
for/whileloop instead ofPromise.all. → seereferences/fetching.md. - Using
useStatefor a mutation's pending flag instead ofuseTransitionoruseMutation's ownisPending. - Forgetting
keepPreviousDataon a filtered/paginated list query — every keystroke blanks the table instead of keeping prior rows visible.