Lesson 8 of 26
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.
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
useEffectinto 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
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.What does 'staleness' mean for server state, and why can't you just fetch once and treat the result as fixed?
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