Skip to main content
TanStack Query in 2026: What Server Components Didn't Replace

TanStack Query in 2026: What Server Components Didn't Replace

July 11, 2026
Frontend Engineering
4 min read

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.