Engrain by ByteRush

RCT.09.02 Mid-level· 12 minCore — asked often

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();
This example uses Node.js, so it runs in our checks rather than in your browser.

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.

One request, shared, then invalidatedStep 1 of 4

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
  1. 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.
  2. The data arrives and is cached as fresh for staleTime (60 s). A fourth component gets it instantly.
  3. The user cancels an order. The mutation succeeds and calls invalidateQueries({ queryKey: ["orders"] }).
  4. 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?

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.”

Stays on your device. Nothing is uploaded.
Follow-up Why use TanStack Query instead of fetching in useEffect?
Caching, de-duplication, stale-while-refetching, retries, cancellation, pagination and devtools, without hand-written race handling.
Follow-up How do you implement optimistic updates with rollback?
onMutate: cancel queries, snapshot, setQueryData; onError: restore the snapshot; onSettled: invalidate.
Follow-up staleTime vs gcTime?
staleTime: how long data is fresh. gcTime: how long unused data stays in memory before being removed.

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”.
Your solution
Ctrl/⌘ + Enter

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
Put every variable the query depends on in its key, such as ["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
Stale queries refetch in the background on mount, window focus and reconnect, while showing cached data Source: TanStack Query: Important defaults.
Mutations change data on the server and then invalidate keys.
Check yourself: When does TanStack Query refetch stale data by default?

On mount, window focus and network reconnect.

Edge cases and trapsWhere people slip
During an optimistic update, an older refetch could land and overwrite your optimistic data, so cancel queries for that key first.
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
Prefetch on hover or at the route level, and with server rendering, prefetch on the server and hydrate the client cache so there is no loading flash. SWR offers a smaller API with the same core ideas (TanStack Query 5.105.0) Source: SWR.
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?