Server State Management and How TanStack Query Fits In
Muhammad Athar

If you've read Why I Stopped Using useEffect for Data Fetching by Alex Chen, you've seen TanStack Query recommended as the replacement for the classic useEffect + fetch pattern. Alex calls it "not just a caching library" and says it's a "server state manager."
And if you're like most developers reading that for the first time, you probably thought: what does server state manager actually mean? Isn't all state just state?
It's a fair question. The answer changes how you think about data in React applications — and once it clicks, a lot of decisions about where to put what become much clearer.
Client state vs server state
Let's start with the distinction that makes TanStack Query make sense.
Client state is state that lives entirely in your application. The user's current theme preference. Whether a modal is open. What they've typed into a search input. You own this data completely. It doesn't exist anywhere else. When the user closes the tab, it's gone.
Server state is data that lives on a server. A user's profile. A list of products. An order history. Your application fetches a copy of it and displays it. But the authoritative version lives somewhere else — on a database, behind an API endpoint — and it can change at any time, independently of what your component is doing.
This difference has real consequences:
Most state management libraries — Redux, Zustand, Jotai — were designed for client state. They're great at it. But they have no concept of staleness, no built-in fetching, no deduplication, no cache invalidation. Using them to manage server state means manually implementing all of that yourself.
TanStack Query was built specifically for server state. It treats fetching, caching, synchronising, and updating remote data as first-class concerns — not things you bolt on.
What TanStack Query actually does
At its core, TanStack Query gives you three things:
A cache. Every piece of server data is stored by a key. When two components ask for the same data using the same key, they both get the same cached copy — and only one network request is made.
Automatic staleness. Cached data has a lifetime. After that lifetime, TanStack Query considers it stale and refetches it in the background next time it's needed. You control how long data stays fresh.
Lifecycle management. Loading, fetching, success, error, refetching — all derived automatically from the state of the request. You don't manage these with useState.
Here's what that looks like compared to the useEffect approach:
Same result. The second version has no race condition, automatic caching, and deduplication built in. Three useState calls and a cleanup function gone.
The query key is the cache key
The queryKey array is the most important concept to understand. It's how TanStack Query identifies and organises cached data.
When userId changes in your component, the queryKey changes, and TanStack Query looks up a different cache entry. If it has cached data for the new key, it returns it immediately while refetching in the background. If not, it fetches fresh.
This is the behaviour that makes navigating between detail pages feel instant — the data for a previously-visited profile is already in the cache.
Think of the query key as an address. The same address always returns the same data. Different address, different data.
Building something real: a user profile card with live data
Here's a complete user profile card that uses TanStack Query for data fetching, with loading and error states, a manual refetch button, and a visual indicator showing when data is stale and being refreshed in the background.
First, the install and setup — wrap your app in a QueryClientProvider:
Now the profile card:
Several TanStack Query behaviours are visible in this widget:
isLoading vs isFetching — isLoading is true only on the very first fetch when there's no cached data. isFetching is true whenever a request is in flight — including background refetches. This distinction lets you show a full skeleton on first load and a subtle progress bar on subsequent refreshes, without the jarring experience of replacing visible content with a skeleton every time data updates.
dataUpdatedAt — a timestamp of when the cached data was last successfully fetched. Used here to show "Updated 12s ago" — the kind of detail that builds user trust in live data.
refetchOnWindowFocus — when the user switches to another tab and comes back, TanStack Query automatically refetches stale data. The profile is always fresh when the user returns to it.
refetch() — a manual trigger. The "Refresh" button gives users control over when to pull fresh data without needing to reload the page.
Mutations: changing server state
Reading data is useQuery. Changing data is useMutation. Here's how updating a user's role looks:
The mental model shift
The thing that makes TanStack Query feel different from useEffect + useState isn't just less code. It's a different mental model for what your component is doing.
With useEffect: your component is responsible for fetching data, managing loading state, handling errors, and keeping everything in sync.
With TanStack Query: your component declares what data it needs. The library handles fetching, caching, synchronisation, and error states. Your component just consumes the result.
That shift — from imperative management to declarative consumption — is what "server state manager" actually means. Your components stop being data managers and go back to being what they're supposed to be: UI descriptions.
Where this leads
Optimistic updates — updating the UI before the server confirms the mutation, rolling back if it fails. TanStack Query's onMutate and onError callbacks make this straightforward. Alex covers the React side of this in Optimistic UI Updates with Server Actions and useOptimistic.
Infinite queries — useInfiniteQuery for paginated lists and infinite scroll. The same cache, the same staleness model, but accumulating pages of results rather than replacing them.
Suspense mode — useQuery with suspense: true integrates directly with React's Suspense boundaries. The component suspends while data is loading and renders when it's ready — no isLoading checks needed. Covered in Alex's How to Stream UI from the Server with loading.tsx and Suspense Boundaries.
This article was prompted by the React Foundations series — particularly Why I Stopped Using useEffect for Data Fetching by Alex Chen, where TanStack Query is recommended without a full explanation of the mental model behind it. If the user profile card above sparked something, the natural next read is How to Build a Custom Hook That Actually Earns Its Abstraction — which shows how TanStack Query fits inside a well-designed hook.
Muhammad Athar is the founder and engineer behind DesignDev.io. He writes the "From the Builder" series — concept explainers triggered by the site's own articles, always grounded in something you can actually build.

