TanStack Query in 2026: What Server Components Didn't Replace
Table of Contents
About the Author

Ahmed Mahmoud
Author & Developer
Software engineer passionate about web development and user experience design.
Founder of Devya · eng-ahmed.com ↗RSC changed the default, not the whole job
When React Server Components and Server Actions matured, the common claim on our radar was that a client data-fetching library was no longer necessary. After two years of shipping App Router applications at Devya, we've found that half true. Server Components replaced the default fetch, not the entire job. This is where we still reach for TanStack Query, and where we've removed it.
What Server Components and Actions genuinely replaced
For a page's initial data, a Server Component that awaits the database or an API is simpler than anything a client library offers: no loading state, no client-side request waterfall, no serialization boilerplate, and the payload never becomes JSON the browser has to re-parse. For straightforward create, update, and delete flows, a Server Action paired with revalidatePath covers the loop without a mutation hook or a query key. When an application is mostly forms and page-level reads, we ship it without a client cache — the result is smaller and easier to reason about, and we have removed the library from projects that fit this shape without missing it.
```tsx // app/dashboard/page.tsx — no client library involved export default async function Dashboard() { const projects = await db.query.projects.findMany(); return <ProjectList projects={projects} />; } ```
Where a client cache still earns its place
Every case where we kept TanStack Query shares one trait: the data changes on the client's timeline, not the request's. Server Components render once per request. The moment the interface needs to poll, refetch, paginate, or share cached data across routes without a full navigation, a client cache becomes the right tool. Polling and near-real-time widgets need a refetch interval that has no Server Component equivalent. Infinite scroll benefits from useInfiniteQuery managing cursors and in-flight deduplication. Data read by several routes — the current user, organisation settings — is served instantly from a shared cache on client navigation. And when a single mutation should optimistically update a list, a counter, and a header badge together, a shared cache is cleaner than wiring several independent optimistic states.
The pattern that makes them coexist: hydration
The early mistake is treating this as an either-or choice. The strongest setup uses both: fetch on the server, then hand the result to TanStack Query so the client cache starts warm and the first render carries no loading state. We prefetch the exact query key on the server, dehydrate it, and wrap the client tree in a HydrationBoundary. The client reads the same key, renders instantly from the hydrated cache, and takes over freshness from there.
```tsx // Server Component const qc = new QueryClient(); await qc.prefetchQuery({ queryKey: ['projects'], queryFn: () => db.query.projects.findMany() }); return ( <HydrationBoundary state={dehydrate(qc)}> <ProjectsClient /> </HydrationBoundary> ); ```
Mutations: revalidatePath versus invalidateQueries
This causes the most confusion in review. Both invalidate caches, but different caches. revalidatePath asks Next.js to re-render Server Components for a route; invalidateQueries asks TanStack Query to refetch a client query. If a value lives in the client cache, a Server Action that only calls revalidatePath will not touch it. Our rule is to assign one owner per piece of data: if a value is read through useQuery, the mutation that changes it must invalidate that query rather than relying on revalidatePath to keep the client cache honest.
Our current decision rule
Start without it. Render reads in Server Components, write through Server Actions, and add TanStack Query only when a concrete need appears — polling, infinite scroll, a shared client cache across routes, or multi-component optimistic updates. Adding it later is a small, local change; removing a cache the application never needed is not. Framed two years ago as Server Components versus client fetching, the two in fact answer different questions: one owns the initial server render, the other owns client-timeline freshness. The applications we are most satisfied with use each for exactly what it does best.
Further Reading
Frontend Engineering
Modern CSS Replaced Our Layout JavaScript: Container Queries, :has(), and Subgrid in Production
Container queries, :has(), subgrid, and cascade layers let our team delete most of the layout JavaScript we used to ship — ResizeObserver wrappers, parent-class toggles, and height-measuring passes. Here is what each feature replaces, and the two we still treat as progressive enhancement.
Frontend Engineering
AbortController Beyond fetch: Cancellation Patterns We Use in Every React App
Uncancelled fetches do not fail loudly — they resolve late and overwrite fresh state. The AbortController patterns we use in every React app we ship: useEffect cleanup, AbortSignal.timeout(), AbortSignal.any(), and one-signal listener cleanup.