6 min read

Server Components Did Not Replace TanStack Query

Mehdi Rezaei
Mehdi
Author
Engineering
Software
Technology

React Server Components did not replace TanStack Query. On a Next.js App Router app, Server Components own the request boundary and the first HTML you ship. TanStack Query v5 owns what happens after that HTML is interactive: polling, refetch-on-focus, infinite scroll, optimistic updates, and any client mutation that must keep a live cache coherent. If you deleted Query because “we have RSC now,” you removed the tool that tracks time on the client.

I still see teams treat this as a binary: either fetch in a Server Component, or keep a client cache. Production dashboards need both. The first paint is a server problem. The next twenty minutes of the session are a client-timeline problem. Those are different jobs.

What React Server Components actually own

A Server Component runs on the server for the initial render and again on navigations that hit that route tree. It can read cookies, hit your database, call an internal API, and stream HTML through Suspense. On the Next.js App Router side, cache control lives next to that model: revalidatePath, revalidateTag, and route-segment caching decide when the next request gets a fresh server tree.

That is excellent for content pages, marketing shells, checkout steps that must be authoritative before paint, and any read that should not wait for a client bundle. It is also a poor fit for “keep this table current while the tab stays open.” A Server Component does not sit in the browser listening for focus events. It does not merge an optimistic row into a list while the mutation is in flight. When the user comes back from another tab, revalidatePath on last week’s publish does nothing for this session.

What TanStack Query still owns

TanStack Query is a client cache with a clock. Keys, staleTime, refetchInterval, refetchOnWindowFocus, infinite queries, and invalidateQueries exist because browser sessions keep changing after the first response. The library answers questions RSC cannot: is this key stale right now, should I refetch because the window focused, how do I page an infinite list without losing scroll state, and how do I roll back an optimistic update when the mutation fails?

Concrete cases where removing Query hurts immediately:

  • A live ops board that polls every 15–30 seconds while the tab is visible.

  • A filterable admin table that must refetch when the user returns from another app.

  • Infinite scroll over a large activity feed with cursor pages already in cache.

  • A create/edit form that paints the new row before the network round trip finishes, then rolls back on error.

You can fake a few of those with router.refresh() and a homemade EventSource. You will rebuild half of Query’s cache semantics badly, then rediscover invalidateQueries under another name.

The handoff that makes them complementary

The supported pattern in TanStack Query v5 with the App Router is prefetch on the server, dehydrate, then HydrationBoundary. A Server Component creates a per-request QueryClient, warms the keys you need for first paint, and passes dehydrated state into a Client Component subtree. Matching useQuery / useSuspenseQuery hooks on the client read the same keys without a loading flash.

TypeScript
1// app/dashboard/page.tsx
2import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
3import { DashboardClient } from './dashboard-client'
4import { getMetrics } from './api'
5
6export default async function DashboardPage() {
7 const queryClient = new QueryClient({
8 defaultOptions: { queries: { staleTime: 60_000 } },
9 })
10
11 await queryClient.prefetchQuery({
12 queryKey: ['metrics'],
13 queryFn: getMetrics,
14 })
15
16 return (
17 <HydrationBoundary state={dehydrate(queryClient)}>
18 <DashboardClient />
19 </HydrationBoundary>
20 )
21}

Two details from the official Advanced SSR guide matter in practice. First, the docs set a default staleTime of 60 * 1000 (60 seconds) under SSR so the client does not immediately refetch the data you just hydrated. Second, as of TanStack Query v5.40.0, pending queries can be dehydrated too: you can void prefetchQuery, stream the shell, and let the client pick up the in-flight promise without awaiting every prefetch before HTML starts.

That handoff is the whole point. RSC gets you a correct first payload. HydrationBoundary seeds the client cache. Query then owns focus refetches, polling, and invalidateQueries after a mutation—without asking the server tree to re-render the entire route for every keystroke of freshness.

Where teams get the ownership wrong

The failure mode is dual ownership of the same bytes. If a Server Component renders posts.length from an awaited prefetch, and a Client Component also renders that list from useQuery, the two views diverge the moment Query refetches. The docs are blunt: treat Server Components as a place to prefetch for hydration, not as a second live consumer of the same query result. Prefer one owner for each surface after mount.

Another common mistake is calling revalidatePath and assuming the open tab updates. revalidatePath invalidates the Next.js cache for the next request or navigation. It does not push into an already-mounted Query cache. After a Server Action mutation, you usually need both: revalidatePath (or a tag) for the next server render, and queryClient.invalidateQueries for the client trees that stay mounted. Skip either side and someone will swear publish “worked” while their screen still shows yesterday.

Navigation freshness is related but separate. Instant shells and prefetch budgets decide how fast a route can appear; they do not decide how a live widget stays current after arrival. If you are drawing route anatomy for click-time shells, keep that contract beside—not instead of—your Query ownership map. See Instant Navigation Is a Contract, Not a Prefetch Setting for the shell side of that split: https://mehd.ir/posts/instant-navigation-is-a-contract-not-a-prefetch-setting

A practical split you can enforce in review

Use this as a PR checklist, not a manifesto:

  • Server Components: first HTML, auth-gated reads, SEO content, and anything that must be true before paint.

  • TanStack Query: anything that must stay fresh while the tab lives—polling, focus refetch, infinite pages, optimistic UI.

  • Handoff: prefetchQuery → dehydrate → HydrationBoundary with matching query keys and a non-zero staleTime.

  • Mutations: Server Actions (or API routes) plus invalidateQueries for mounted clients; revalidatePath / tags for the next server render.

  • Never render the same live query result in both a Server Component and a Client Component after mount.

Start a greenfield App Router app with the framework fetch and cache tools. Add TanStack Query when a screen develops a client timeline: tabs that stay open, lists that page forever, or mutations that must feel local. Keep both when the product needs a correct first paint and a correct next minute. RSC replaced a class of client waterfalls. It did not replace a cache that knows what time it is in the browser.

Share this article