Server-state libraries: TanStack Query and SWR
Why server data needs a cache library: query keys, stale time, background refetching, de-duplication, mutations with invalidation, optimistic updates with rollback, infinite and paginated queries, prefetching and SSR hydration.
Comes up in: React, Frontend system design, Machine coding
Learn#
Nikhil’s app fetches the user’s orders in four components, each with its own effect. Opening the page sends four identical requests. The counts disagree for a moment. After a cancel, the header still shows the old count.
Server state needs a cache that knows how old each piece of data is. TanStack Query and SWR provide one. Each query has a query key: an array that names the data, such as ["orders", { status: "open" }]. The stale time says how long data counts as fresh:
import { QueryClient } from "@tanstack/query-core";
const client = new QueryClient();
let requests = 0;
const fetchOrders = async () => { requests++; await new Promise((r) => setTimeout(r, 20)); return ["Tea", "Rice"]; };
const orders = { queryKey: ["orders", { status: "open" }], queryFn: fetchOrders, staleTime: 60_000 };
// Three components ask at the same time: one request.
await Promise.all([client.fetchQuery(orders), client.fetchQuery(orders), client.fetchQuery(orders)]);
console.log("after 3 parallel asks:", requests, "request");
// Still fresh (staleTime 60 s): served from the cache.
await client.fetchQuery(orders);
console.log("after a 4th ask:", requests, "request");
// A mutation changed orders: invalidate by key prefix, and the next ask refetches.
await client.invalidateQueries({ queryKey: ["orders"] });
await client.fetchQuery(orders);
console.log("after invalidation:", requests, "requests");
client.clear();Output (verified)
after 3 parallel asks: 1 request after a 4th ask: 1 request after invalidation: 2 requests
De-duplication shares one in-flight request among all callers. Invalidation marks cached data stale after a change, so it refetches.
Network
- GET /orders?status=open
Cache
- orders/open: fetching (3 observers)
Header, sidebar and page all ask for ["orders", { status: "open" }] at once. The cache has nothing, so one request starts and all three share it.
Read the steps as text
- Header, sidebar and page all ask for ["orders", { status: "open" }] at once. The cache has nothing, so one request starts and all three share it.
- The data arrives and is cached as fresh for staleTime (60 s). A fourth component gets it instantly.
- The user cancels an order. The mutation succeeds and calls invalidateQueries({ queryKey: ["orders"] }).
- Stale queries on screen refetch in the background; the old list stays visible until the new one arrives.
Predict#
Predict first
With TanStack Query's default staleTime of 0, a user switches to another browser tab and back. What happens to queries on screen?
Stale data is still shown, but focus, reconnect and new mounts trigger a background refetch. Raise staleTime for data that rarely changes, so it stops refetching so often. The cached data means no spinner.
Interview angle#
Say it like this — about 30 seconds
“Fetching in an effect gives you no cache, no sharing and no race handling. TanStack Query caches data by query key. It shares requests that are in flight, and shows cached data while it refetches. It also retries failures and refetches on focus. After a change, I invalidate the related keys so they refetch. For an optimistic update, I cancel outgoing queries and save a snapshot. Then I write the new data, roll back to the snapshot on error, and invalidate at the end.”
Follow-up Why use TanStack Query instead of fetching in useEffect?
Follow-up How do you implement optimistic updates with rollback?
Follow-up staleTime vs gcTime?
Practice#
On your own
Build a tiny query cache: keyed data, freshness, de-duplication, invalidation by prefix, and direct writes.
Done when
- Parallel requests for one key call the fetch function once.
- Fresh data is served without fetching; stale or invalidated data refetches.
invalidate(["orders"])affects every key that starts with “orders”.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
What identifies a query in TanStack Query?
Its query key, an array such as ["orders", { status: "open" }].
What does staleTime control?
How long data counts as fresh, without background refetches.
What should a mutation do after it succeeds?
Invalidate the related query keys.
Steps of an optimistic update with TanStack Query?
Cancel queries, snapshot, set new data; on error restore; on settle invalidate.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
["orders", userId, filters]. Keys are compared by value and work as prefixes for invalidation.Check yourself: Why put every input of a query in its key?
So different inputs get different cache entries and the query refetches when they change.
How it works underneathSenior rounds
Check yourself: When does TanStack Query refetch stale data by default?
On mount, window focus and network reconnect.
Edge cases and trapsWhere people slip
useInfiniteQuery stores pages with cursors (WEB.15.01); placeholderData: keepPreviousData keeps the old page visible while the next loads.Check yourself: Why cancel queries in onMutate?
So an in-flight refetch does not overwrite the optimistic data with old data.
In productionStaff-level answers
Check yourself: How do you avoid a loading flash after server rendering?
Prefetch on the server and hydrate the cache on the client.
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-11-01. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.
Interview questions this answers
- Why use TanStack Query instead of fetching in
useEffect? - How do you implement optimistic updates with rollback?