Headline: When React Server Components and Server Actions landed, the loudest take was "you don't need a client data-fetching library anymore." After two years of shipping App Router apps, that's half true. RSC replaced the default fetch, not the whole job â here's where I still reach for TanStack Query, and where I've deleted it.
What RSC and Server Actions actually replaced
For a page's initial data, a server component that awaits your database is simpler than anything a client library gives you: no loading state, no client-side waterfall, no serialization boilerplate, and the data never becomes JSON the browser re-parses.
// app/dashboard/page.tsx â no client library involved
export default async function Dashboard() {
const projects = await db.query.projects.findMany();
return <ProjectList projects={projects} />;
}
For simple mutations, a Server Action plus revalidatePath covers create/update/delete without a mutation hook or a query key. If your app is mostly forms and page-level reads, ship it without a client cache â it'll be smaller and easier to reason about. I deleted TanStack Query from two projects that fit this shape and didn't miss it.
Where a client cache still earns its place
Every case where I 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 UI needs to poll, refetch, paginate, or share cached data across routes without a full navigation, you're back in client-cache territory.
-
Polling / near-real-time. A build-status widget needs
refetchInterval. There's no server-component equivalent to "re-run every few seconds without navigating." -
Infinite scroll.
useInfiniteQuerymanages cursors, dedupes in-flight requests, and keeps prior pages mounted. - Cross-route cache sharing. When several routes read the same current user or org settings, a shared cache serves them instantly on client navigation.
-
Optimistic updates across components.
useOptimisticis great for one form; when a mutation should optimistically touch a list, a counter, and a header badge at once, a shared cache withsetQueryDatais cleaner.
The pattern that makes them coexist: hydration
The mistake I made early was treating it as either/or. The best setup uses both â fetch on the server, hand the result to TanStack Query so the client cache starts warm and the first render has zero loading state.
// app/projects/page.tsx (Server Component)
const qc = new QueryClient();
await qc.prefetchQuery({
queryKey: ['projects'],
queryFn: () => db.query.projects.findMany(),
});
return (
<HydrationBoundary state={dehydrate(qc)}>
<ProjectsClient />
</HydrationBoundary>
);
The client reads the same query key, renders instantly from the hydrated cache, and owns freshness from there â polling, refetch-on-focus, and invalidation all work as usual.
Mutations: revalidatePath vs invalidateQueries
This is where I see the most confusion. Both invalidate caches â different caches. revalidatePath re-renders server components for a route; invalidateQueries refetches a client query. If your data lives in the client cache, a Server Action that only calls revalidatePath won't touch it.
const { mutate } = useMutation({
mutationFn: (input: NewProject) => createProject(input), // server action
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['projects'] }),
});
My rule: one owner per piece of data. If a value is read through useQuery, the mutation that changes it must invalidate that query â don't rely on revalidatePath to keep the client cache honest.
My current decision rule
Start without it. Render reads in server components, write through Server Actions, and add TanStack Query only when a concrete need shows up: polling, infinite scroll, a shared client cache across routes, or multi-component optimistic updates. Adding it later is a small, local change; ripping out a cache you didn't need is not. RSC and a client cache answer different questions â one owns the initial server render, the other owns client-timeline freshness â and the apps I'm happiest with use each for exactly what it's good at.
Originally published on devya.dev. Also on eng-ahmed.com. Built by Devya Solutions.
Top comments (0)