Skip to the chapter
←ReactGGroundwork⚛React
React/Intermediate/I14
25 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛I14 · Part 2 · Intermediate

Server state with TanStack Query

Treat API data as a cache you do not own, and most of your state problems disappear.

3 min read9 sections4 exercises
25 of 57

In this chapter

  • The distinction the library is built on
  • The basic query
  • The query key is the cache key
  • staleTime is the setting that matters
  • Mutations and invalidation

and 4 more

about 3 min
09On this page
  1. The distinction the library is built on
  2. The basic query
  3. The query key is the cache key
  4. staleTime is the setting that matters
  5. Mutations and invalidation
  6. Optimistic updates, with the rollback
  7. Paginated and infinite lists
  8. Prefetching removes the spinner
  9. SWR, and where this fits with frameworks

The distinction the library is built on

Client stateServer state
Owned byYouSomebody else's database
Can go staleNoConstantly
Needs loading and error statesNoAlways
Shared between componentsSometimesUsually the same copy
ExamplesIs the menu open, the current tabUsers, orders, anything fetched

Putting server state in useState means reimplementing caching, deduplication, revalidation and invalidation in every component that fetches. That is the entire argument.

The basic query

const { data, isPending, isError, error } = useQuery({
  queryKey: ["user", userId],
  queryFn: () => fetch("/api/users/" + userId).then((r) => r.json()),
});

if (isPending) return <Skeleton />;
if (isError) return <ErrorPanel error={error} />;
return <Profile user={data} />;

Three components asking for ["user", 7] make one request and share the result. Navigate away and back and the cached value renders immediately while a refetch happens quietly behind it.

The query key is the cache key

["todos"]                             // all todos
["todos", { status: "done" }]         // a filtered list
["todos", todoId]                     // one todo

Everything the query depends on goes in the key. Change the key and it is a different entry — which means refetching is automatic, and the race condition cannot happen, because responses are stored against the key they were requested with rather than written into shared state.

Keys are hierarchical, and that is what makes invalidation ergonomic: invalidating ["todos"] invalidates every key that starts with it.

staleTime is the setting that matters

useQuery({ queryKey, queryFn, staleTime: 5 * 60 * 1000 });   // fresh for 5 minutes
SettingMeansDefault
staleTimeHow long data is considered fresh — no refetch at all0
gcTimeHow long unused data stays in memory before eviction5 min
refetchOnWindowFocusRevalidate when the tab regains focustrue

With the default staleTime: 0, every mount revalidates. People meet the library, see a request on every navigation, and conclude it is chatty. It is doing exactly what it was told; a country list can have a staleTime of an hour, a live price cannot.

Mutations and invalidation

const queryClient = useQueryClient();

const { mutate, isPending } = useMutation({
  mutationFn: (todo) => api.post("/todos", todo),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ["todos"] });   // refetch the lists
  },
});

mutate({ title: "Write the chapter" });

Invalidation is the half people get wrong. After a create, which keys are now out of date? The list, certainly — but also any filtered list, any count, any dashboard summary. Hierarchical keys mean one invalidateQueries(["todos"]) covers all of them, which is why key design is worth five minutes of thought at the start.

Optimistic updates, with the rollback

useMutation({
  mutationFn: toggleTodo,
  onMutate: async (id) => {
    await queryClient.cancelQueries({ queryKey: ["todos"] });   // stop a refetch overwriting us
    const previous = queryClient.getQueryData(["todos"]);
    queryClient.setQueryData(["todos"], (old) =>
      old.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
    );
    return { previous };                                        // context for onError
  },
  onError: (err, id, ctx) => {
    queryClient.setQueryData(["todos"], ctx.previous);           // roll back
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ["todos"] });      // reconcile either way
  },
});

The checkbox ticks instantly, and if the request fails it un-ticks and the error is shown. Naming the rollback in an interview is what separates having read about optimistic updates from having shipped one.

Paginated and infinite lists

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
  queryKey: ["todos"],
  queryFn: ({ pageParam }) => fetchPage(pageParam),
  initialPageParam: 0,
  getNextPageParam: (last) => last.nextCursor ?? undefined,
});

For classic pagination, placeholderData: keepPreviousData keeps the current page on screen while the next loads, so the table does not collapse to a spinner between pages.

Prefetching removes the spinner

<Link
  onMouseEnter={() => queryClient.prefetchQuery({ queryKey: ["user", id], queryFn })}
  to={"/users/" + id}
/>

By the time the click registers, the data is usually there. It is one of the cheapest perceived-performance wins available.

SWR, and where this fits with frameworks

SWR is the smaller alternative with the same core ideas — a key, a fetcher, stale-while-revalidate — and less surface area. Either is a good choice; TanStack Query has more built in for mutations and pagination.

In a framework with Server Components or loaders, most initial data should come from the server instead, and the query cache handles what stays interactive — polling, infinite scroll, anything after a user action. The two compose: the server can hydrate the cache so the first render has data and later interactions still get caching and invalidation.

Interview answer, one sentence

"Server state is a cache of data you do not own, so it needs staleness, deduplication, revalidation and invalidation — none of which useState provides. The query key is the cache key, so a changed key refetches automatically and the response race disappears; the hard part left is deciding which keys a mutation invalidates."

04Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsHash a query keyintermediate
  • 9 testsAn optimistic like button that rolls backintermediate
  • 8 testsLoad more, with cursorsintermediate
  • 4 testsWarm the cache before the click, not afterintermediate
All 66 for this level →
I14

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. I13Previous · I13Auth in React
  2. I14you are here
  3. Next · I15Testing ReactTest what a user can do. A test that breaks on a refactor was protecting the wrong thing.4 min read

01On this page

9 sections · 0%

  1. 01The distinction the library is built on
  2. 02The basic query
  3. 03The query key is the cache key
  4. 04staleTime is the setting that matters
  5. 05Mutations and invalidation
  6. 06Optimistic updates, with the rollback
  7. 07Paginated and infinite lists
  8. 08Prefetching removes the spinner
  9. 09SWR, and where this fits with frameworks

[] chapters · t top