AniUI Academy

Server State vs. Client State

Why data that lives on a server is a fundamentally different problem from UI state — caching, staleness and revalidation — and why treating them the same causes bugs.

12 min read

It's tempting to treat all application state the same way: a useState call or a slot in a global store, regardless of whether the value is "is this modal open" or "the list of orders fetched from the API." That temptation is where a lot of real-world data-fetching bugs come from, because these are genuinely different problems wearing the same syntax.

What actually makes them different

Client state is owned entirely by your application, in this session. A modal's open/closed flag, a form's current input value, which tab is selected — all of it changes only because something happened locally (a click, a keystroke), and it's never "wrong" in the sense of being out of date with some external source of truth, because there is no external source of truth for it.

Server state is a copy of data that actually lives somewhere else, and critically, that somewhere else can change it without your app knowing. Two tabs open to the same order; another user editing the same document; a price that updates on the backend while your page sits open. The data your component is holding can silently drift from what's actually true, and that gap is the entire problem server-state tools exist to manage.

Client state

Owned by this session. Changes only in response to local interaction. Never 'stale' — it's authoritative for itself.

Server state

A cached copy of data owned elsewhere. Can go stale the instant it's fetched, because the server can change it independently of anything happening in your app.

The problems server state genuinely has that client state doesn't

Staleness. The moment a fetch resolves, the data it returned is already a snapshot, not a live view — the real question is how stale is acceptable for this particular piece of data (a stock ticker: not at all; a user's bio: plenty), not whether staleness exists.

Revalidation. Deciding when to refetch — on mount, on window refocus, on a timer, after a related mutation, on user-triggered refresh — is a real design decision with real trade-offs between freshness and unnecessary network traffic, and it doesn't have an equivalent in client state at all.

Request deduplication. If three components on the same page each ask for the same resource, should that fire three network requests? Server-state tools typically dedupe automatically; hand-rolled fetching in component effects usually doesn't, unless you build that yourself.

Loading and error as first-class states, per resource — not just "is the whole page loading," but per-query state that a UI can react to independently.

Cache invalidation after mutation. After a "delete this comment" action succeeds, the comment list that's cached elsewhere in the app needs to know it's now stale — a coordination problem that pure client state never has to solve, because client state has no other copies to invalidate.

Why using one tool for both tends to go wrong

Reaching for useState plus a useEffect fetch, or dumping API responses into the same general-purpose global store as UI flags, means re-deriving all of the above by hand: writing your own staleness timers, your own refetch-on-focus listener, your own deduplication guard, your own invalidation calls scattered across every mutation that could affect cached data. It's not that it's impossible — it's that libraries built specifically for server state (React Query, SWR, Apollo Client's cache, RTK Query) already solved this well, and hand-rolling it usually means solving it worse, later, under time pressure, after the bug report about a stale price arrives.

The practical dividing line worth stating in an interview: if a piece of state can become "wrong" because something outside your current session changed it, it's server state, and it deserves a tool that treats staleness and revalidation as first-class concerns. If it only ever changes because of something that happened in this session, it's client state, and a store built for caching remote data is solving a problem you don't have.

What to remember

  • Server state is a cached copy of data owned elsewhere and can go stale independently of anything happening in your app; client state is owned by the current session and is never stale in that sense.
  • Server-state problems — staleness, revalidation, deduplication, per-resource loading/error, cache invalidation after mutation — have no client-state equivalent.
  • Fetching in a useEffect into local or global state re-derives all of this by hand, usually worse than a tool built specifically for it.
  • Ask "can something outside this session make this value wrong?" to decide which kind of state you're actually managing.

Check yourself

3 questions · pass 3/3 to unlock Optimistic Updates and Rollback

up to 50
  1. 1.Why is 'the list of products fetched from an API' a genuinely different kind of problem from 'whether a sidebar is currently open'?

  2. 2.What does 'staleness' mean for server state, and why can't you just fetch once and treat the result as fixed?

  3. 3.A team stores fetched API data in the same global store as UI flags like isSidebarOpen, using the same actions/reducers for both. What problem does this conflation typically cause?

3 left to answer