Skip to content
Chapter 76Lesson 1

When to reach for TanStack Query

Decide which workloads need a client-side server-state library like TanStack Query, and which the React and Next.js defaults already cover.

You have learned where state lives without installing a single state library. Read state goes to Server Components, write state to Server Actions, transient UI state to useState, and shareable view state into the URL with nuqs. Between them, those four defaults cover almost the entire surface of a SaaS app.

This chapter introduces TanStack Query, the most popular data-fetching library in the React ecosystem, and you will reach for it rarely, because the four defaults already handle most of what you would have used it for.

This lesson teaches no TanStack Query code: no useQuery, no setup, no API. It builds one habit: look at any feature and ask whether it earns a client-side server-state library, or whether a default already covers it. Get the threshold right and the rest of the chapter is mechanical; get it wrong and the library scatters across screens that never needed it, and the cost lands on the codebase six months later.

For a client-side server-state library to earn its weight, you need a workload that none of your four defaults does well, and that band is far narrower than most developers assume.

The four platform defaults, the state each owns, and where the student met it
Default The state it owns Where you met it
Server Components Server data, read and streamed into the page the App Router chapters
Server Actions Mutations, with revalidation for read-your-writes the Server Actions chapter
useState Transient UI state inside one component the React chapters
nuqs (URL) Shareable, refreshable view state the URL-state chapter
The four state defaults and the workload each one owns.

Filtering a table, submitting a form, paginating a list, reading the signed-in user: every one resolves to a cell above, and none is a TanStack Query question.

The idea the chapter hangs on: TanStack Query is conditional, not default. It is not “how you fetch data in React”; that mental model predates Server Components and Server Actions, and this chapter replaces it.

Exactly four workloads clear the threshold: polling, cross-view caching, optimistic mutations into a cache, and infinite scroll with reuse. The term that ties them together is server-state : rows that live on your server. It becomes a client-side problem when the client owns the cache and refetch lifecycle of that data, because the interaction is too live, too cache-heavy, or too optimistic for a server round-trip to drive it. A client-side server-state library is the tool for exactly that situation.

Hold that against its opposite. Whether a dropdown is open, the half-typed text in a search box, the chosen theme: none of that belongs to the server. That is plain client state, never a TanStack Query question no matter how interactive it feels, a line that seeds a check we run later.

The four triggers that justify TanStack Query

Section titled “The four triggers that justify TanStack Query”

This course accepts four justifications for reaching past the defaults, and no more. If a workload matches none of them, the answer is a default.

  1. Polling and frequent refetches.
  2. Complex client-side caching across views.
  3. Optimistic mutations with rollback into a cached query.
  4. Infinite scroll with cache reuse.

Each trigger is the point where one specific default stops pulling its weight, so it only makes sense against the default it beats.

The workload. A comment thread that should show a coworker’s new message, a notification badge that lights up on its own, a job-status panel that reads “exporting…” then “done.” Each needs the client to learn about a server change on a tight cadence, with no user action.

The default that almost covers it. router.refresh() re-runs the current route segment and streams the updated render back; wire it to a button for a manual pull, or to a setInterval for a cadence.

Why it breaks. It re-runs the entire segment, every fetch and the whole server render, just to learn whether one comment arrived, far too heavy on a five-second interval.

The trigger is met. You want a tool that refetches just that slice and updates just that part of the tree, which is what a client-side cache with a refetch interval does.

The workload. A detail page and a list page that read the same row, a sidebar that mounts and unmounts the same data as the user toggles it, a tab strip that flips between views of one underlying query. Moving between these should not refetch data you already have.

The default that almost covers it. Next.js already caches: the Router Cache keeps prefetched route segments so soft navigations don’t refetch, and use cache handles caching on the server.

Why it breaks. Those caches are scoped to navigations; they do nothing for a client tree that mounts and unmounts the same data within a single view, where you want an explicit client cache keyed by the query so every mount reads the cached value.

The trigger is met, but this is the weakest of the four: if it is your only justification, you are usually one navigation away from the Router Cache handling it.

Optimistic mutations with rollback into a cached query

Section titled “Optimistic mutations with rollback into a cached query”

The workload. An action that must show instantly, roll back cleanly on failure, and whose optimistic value has to land inside one or more cached queries, like a new comment appearing atop a cached, paginated list the moment you hit send.

The default that almost covers it. React 19’s useOptimistic shows an optimistic value immediately and rolls it back if the action fails, which handles the single-list case cleanly.

Why it breaks. It is scoped to one component’s render; it cannot write into a separate cache, survive a navigation, or coordinate with other in-flight mutations.

The trigger is met. The instant your update targets a cached query that other parts of the screen read from, you need the cache’s own optimistic machinery: the distinction is not optimism but optimism into a separate cache.

The workload. A long comment thread, a feed, a chat: scroll down and more loads, scroll back up and the loaded pages are still there, with no refetch.

The default that almost covers it. You have already built paging: the cursor pagination from the lists chapter, which produces the right opaque-cursor URL and a correct hasNext flag.

Why it breaks. Cursor pagination replaces, so each “Next” drops the previous page and scrolling back refetches; infinite scroll accumulates, keeping every page in the client cache for the session so scrolling back is free.

The trigger is met. Same data, different cache contract.

  • Polling / frequent refetches

    Learn about server changes on a cadence, with no user action.

    instead of router.refresh() on an interval

  • Optimistic into a cache

    An optimistic value must land inside a cached query.

    instead of useOptimistic

  • Infinite scroll with reuse

    Accumulate pages, then scroll back without a refetch.

    instead of cursor pagination

  • Cross-view caching

    Re-mount the same data across views without refetching.

    instead of the Router Cache

    weakest trigger
Each trigger paired with the one default it beats.

Four triggers; everything else is a default. The bar is strict because TanStack Query is not free, and its cost is what raises it.

  • A second cache to reason about. The Server Component cache stays, so two caches now hold your data and “where does this live” has two answers on every screen.
  • A second invalidation surface. Server data invalidates with updateTag / revalidateTag, the client cache with invalidateQueries. A mutation touching data both layers hold must invalidate both, or you get the canonical bug: the list paints fresh while the detail stays stale.
  • A second model for “when does this refetch.” Stale time, garbage-collection time, refetch-on-focus: knobs Server Components never made you think about, and now you own them.
  • A runtime dependency. Well-maintained and widely used, but still a dependency in your client bundle that every future maintainer has to understand.
  • A forced Client Component boundary. Every useQuery lives in a Client Component, pulling its whole subtree out of Server-Component land. The library does not just add a cache; it moves the boundary.

The cost is worth paying only where a default has actually failed, which is why it fits the four triggers and nothing else. And it lands on the codebase’s future, not your screen: drop the library into one screen for convenience and the next developer reads it as the house pattern. Six months later, forty useQuerys do work Server Components would have done faster, and every reviewer reasons about two caches. Deciding what doesn’t get the library is the call that matters, and it is made before any code is written.

Non-triggers are far more common than triggers, so this is the skill worth drilling hardest. The procedure: name which default the workload would otherwise use; only if every default is wrong does TanStack Query earn its weight. Name the specific default, not “is this a default?” in the abstract. Here it runs against four workloads that look like they want a fetching library but don’t.

  • A list view with filter, sort, search, and pagination. This is the URL’s job: the state must be shareable and survive a refresh, so nuqs drives Server Components, as the lists chapter built it. Not a client cache.
  • A form that submits and shows field errors. A Server Action holds the mutation contract, useActionState the pending and error state. The Server Actions chapter owns this end to end.
  • A single optimistic toggle: a star, a favorite, a checkbox that flips instantly. useOptimistic covers it: one component, one optimistic value, one rollback path. No separate cache, no library.
  • Reading the current user. This is server-state, but the default reads it once with auth() inside a Server Component. Server-state does not automatically mean client-cached server-state.

Before that enumeration, run one cheaper check: do we even own this on the client? Plenty of state that feels live does not belong to the server at all: URL state, form inputs, theme preference, whether a panel is open. None of it is server-state, so none of it is a TanStack Query question, however dynamic it feels. This catches the most common mistake, reaching for useQuery as a generic state manager; use useState, or Zustand for global client state. Even the optimistic-into-a-cache trigger qualifies only when the rollback target is server-state; an optimistic theme switch is still just client state.

The exercise below gives you SaaS workloads, one per chip. For each, name the default it would use, and sort it into “earns it” only if every default genuinely fails.

For each workload, name the default it would otherwise use — `nuqs`, a Server Action, `useOptimistic`, `auth()`, `useState`. Only if every default is wrong does it land in 'TanStack Query earns it'. Drag each item into the bucket it belongs to, then press Check.

TanStack Query earns it Is it live, optimistic-into-a-cache, or accumulate-and-reuse?
A default already covers it Which default — nuqs, a Server Action, useOptimistic, auth(), or useState?
A job-status panel that polls every 5s until the export finishes
A chat thread the user scrolls deep into, then scrolls back up with no refetch
Posting a comment that must appear instantly at the top of a cached, paginated thread
A notification badge that lights up without anyone clicking
An invoices table with filter, sort, and a shareable URL
An edit-invoice form that submits and shows field errors
A single “mark as favorite” star with instant feedback
Showing the signed-in user’s name in the header
A dark-mode toggle saved to localStorage

The toggle chip is worth dwelling on: a theme preference is not server-state, so it never reaches the four triggers. It falls out at the “do we even own this on the client?” check, one step before the enumeration.

The decision funnel before reaching for TanStack Query

Section titled “The decision funnel before reaching for TanStack Query”

The order matters more than any single answer. The common mistake is to pattern-match “this feels like a fetch” and skip straight to the library; a senior asks the gates in sequence, and the first one the workload genuinely needs picks the tool.

Does this workload earn TanStack Query?

The rest of the chapter sits behind this funnel, and only matters for a workload that reaches one of the TanStack leaves.

Server Components own the first paint, TanStack owns the live cache

Section titled “Server Components own the first paint, TanStack owns the live cache”

Crossing the threshold does not turn the page into a client app that fetches everything with a spinner on load. The page stays a Server Component that prefetches the data and hands it across a hydration boundary, so the first paint is server-rendered and the client cache starts out full. No useQuery fires a cold request on load; only the live parts, the polling, optimistic mutations, and scroll-to-next-page, use the client cache after that. The two systems own different phases of the same screen, so you pay TanStack Query’s price only where it earns it.

A later lesson wires this up; the figure shows the shape now.

  1. Initial paint Server Component prefetch → hydrate
  2. Live interactions TanStack client cache poll · mutate · scroll

first paint over time

Two systems, two phases of the same screen: the Server Component owns the first paint, and the TanStack cache owns the live interactions after it.

The other option is SWR, the older, smaller library from the Vercel team, built around the stale-while-revalidate pattern and good for simple read-and-revalidate cases. This course picks TanStack Query because the four triggers need its richer surface: a real mutation lifecycle with first-class optimistic patterns, infinite queries with a maxPages memory cap, shared mutation state, stronger devtools, and broader ecosystem reach. That mutation and infinite-query story is what the chapter’s worked screen depends on, and what SWR lacks.

A codebase already running SWR for these workloads is fine. The threshold, the four triggers, and the two-cache reality all transfer unchanged; only the API differs, and this course does not teach SWR’s. The decision is the durable part, the library the replaceable one.

The invoice comment thread, a worked example

Section titled “The invoice comment thread, a worked example”

The one surface in our own app that clears the threshold is a per-invoice comment thread on each invoice detail page, where disputes, internal notes, and customer correspondence get attached to the invoice they concern.

Run the funnel against it and it clears the bar honestly. It needs polling, because a coworker posts from another session and you should see it without refreshing. It needs optimism into a cache, because your new comment appears instantly at the top of the cached thread and rolls back on a 500. It needs infinite scroll with reuse, because you can be hundreds of comments deep and scroll back up with no refetch. Three strong triggers on one surface, plus a weak fourth from peeking the same thread in a recent-activity sidebar, make it the course’s strongest case for the library.

And yet most of that page is still Server Components. The header, the customer card, the line items, and the totals are all server-rendered and touch no TanStack Query; only the comment thread crosses into the client cache. That is the shape to keep: not “the page is now a client app,” but “a Server Component with one live leaf.”

The next three lessons cover the primitives this screen reaches for, wire them against the App Router without leaking the cache, and run the full funnel against this exact screen.