Summary
Server state — API responses, database records — is async, shared across components, and has an expiry. TanStack Query and SWR manage it with a client-side cache keyed by query keys, background revalidation, and request deduplication. React Server Components shift some of this work to the server entirely, removing the need for a client cache layer in those cases.
Jump to the interview angleServer state differs from client state (UI toggles, form fields) in four ways: it's async (requires a network round-trip), shared (multiple components observe the same data), remote-owned (the server can change it without the client knowing), and potentially stale (a cached copy drifts from the truth over time). TanStack Query v5 and SWR give each piece of server state a query key — a serialisable identifier — and store the response in an in-process cache. They track loading/error/success so components don't have to, and revalidate in the background automatically.
Server state vs client state
- **Async** — server state always involves a network fetch; client state (modals, form values) is synchronous.
- **Shared** — multiple components read the same cache slot; changing it anywhere propagates everywhere.
- **Remote-owned** — the server mutates it independently; the client only holds a snapshot.
- **Stale-capable** — cached data drifts without explicit revalidation; `staleTime` controls the freshness window.
- **RSC alternative** — React Server Components fetch on the server and ship HTML; no client cache needed for that data.
TanStack Query v5 — useQuery (object syntax)
v5 dropped the positional overload. Every call takes a single options object. queryKey is the cache identity; staleTime sets the freshness window in ms.
import { useQuery } from "@tanstack/react-query";
interface Post { id: number; title: string; }
async function fetchPost(id: number): Promise<Post> {
const res = await fetch(`/api/posts/${id}`);
if (!res.ok) throw new Error("fetch failed");
return res.json();
}
export function PostDetail({ id }: { id: number }) {
const { data, isPending, isError } = useQuery({
queryKey: ["post", id], // cache key — changes when id changes
queryFn: () => fetchPost(id),
staleTime: 30_000, // fresh for 30 s; no background refetch while fresh
});
if (isPending) return <p>Loading…</p>;
if (isError) return <p>Error loading post.</p>;
return <h1>{data.title}</h1>;
}Object-syntax useQuery (v5 required). staleTime: 30_000 prevents redundant refetches for 30 seconds. Changing id creates a new cache entry automatically.
RSC fetch is not the same as client-cache fetch
In Next.js App Router, identical fetch calls within one render are deduped (request memoization, on by default). The Data Cache — persisting results across requests — is off by default; opt in with { next: { revalidate } } or { cache: 'force-cache' }. Fetching the same resource in both an RSC and a client useQuery without coordination sends duplicate requests.
Interview angle
Interviewers test whether you distinguish server state from UI state. Name the four properties (async, shared, remote-owned, stale-capable) and explain query keys as the cache identity. Contrast client-cache libraries with RSC fetch.
Soundbite: "Server state is remote-owned and stale by default — a dedicated cache layer beats manual useEffect wiring every time."
Key terms
- query key
- Serialisable array that identifies a cache entry in TanStack Query or SWR; changing it triggers a new fetch.
- staleTime
- Duration in ms during which cached data is considered fresh and no background refetch fires.
- gcTime
- How long an unused TanStack Query cache entry is kept in memory before garbage collection. Default 5 min.
- background revalidation
- Refetch triggered silently on focus or reconnect; updates the cache without a loading spinner.
- request deduplication
- Multiple components calling `useQuery` with the same key share one in-flight network request.