Frontend / State managers / 01_server_vs_client_state.md

Server State vs Client State

Updated 4 interview angles 5 min read source
On this page10
  1. Define server state and client state.
  2. Why is “put all the API data in Redux” an anti-pattern?
  3. What does a server-state library actually give you?
  4. So what’s left for a client-state library?
  5. Where do RTK Query and TanStack Query fit?
  6. What about derived state — store it or compute it?
  7. Gotchas
  8. Related
  9. Further reading
  10. Interview angle

Server State vs Client State

The single most important framing for modern frontend state: most “state management” pain comes from treating server state and client state as the same problem. Server state is a cache of data that lives on a server — async, shared, owned elsewhere, and can go stale without you knowing. Client state is UI state you own outright — synchronous, local, ephemeral (open menus, form drafts, theme, selected tab). Put server state in a data-fetching/caching library (TanStack Query, RTK Query, SWR). Put client state in useState/Context/Zustand/Redux. Hand-rolling server data into Redux means reimplementing caching, dedup, and refetching — badly.

Define server state and client state.

Server state Client state
Source of truth a remote server the client
Sync model asynchronous (fetch) synchronous
Ownership shared with other clients exclusive to this session
Staleness can become stale silently always current
Examples user profile, product list, comments modal open, form input, theme, sort order

The mismatch: client-state tools assume the value is current and yours. Server data is neither — so it needs caching, invalidation, background refetch, and dedup, which is a different tool’s job.

Why is “put all the API data in Redux” an anti-pattern?

You end up manually writing what a server-state library gives you for free:

  • caching keyed by request,
  • request deduplication (two components asking for the same data → one fetch),
  • background refetch / stale-while-revalidate,
  • loading/error state per query,
  • garbage-collecting unused data,
  • retry/backoff.

That’s hundreds of lines of thunks, loading flags, and normalized reducers reimplementing a cache. Use a purpose-built cache instead.

What does a server-state library actually give you?

Using TanStack Query as the example:

tsx
function Profile({ id }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ["user", id],
    queryFn: () => fetchUser(id),
    // serve from cache for 30s before refetching
    staleTime: 30_000,
  });
  if (isLoading) return <Spinner />;
  return <h1>{data.name}</h1>;
}

Cache keyed by queryKey, automatic dedup, background revalidation, and shared cache across components — no reducers, no loading booleans by hand. See TanStack Query and server state.

So what’s left for a client-state library?

The genuinely-local stuff: which tab is open, multi-step form drafts, a shopping-cart UI before checkout, theme/locale, “is the sidebar collapsed.” This is small, synchronous, and often fine in useState + Context, or a lightweight store (Zustand/Jotai) when shared widely. Reach for Redux only when client state is genuinely complex (see Choosing a State Library).

Where do RTK Query and TanStack Query fit?

Both are server-state libraries. RTK Query (RTK Query) is the Redux Toolkit answer — pick it if you’re already on Redux and want one store/devtools. TanStack Query is framework-agnostic and the default if you have no other reason to run Redux. Using either means you stop storing server data in your client store.

What about derived state — store it or compute it?

Don’t store what you can derive. totalPrice = items.reduce(...), filteredList = list.filter(...) should be computed at render (with the React Compiler that is already memoized; without it, useMemo or a selector if expensive), not duplicated into state where it can drift out of sync with its source. Storing derived state is a top cause of “the count is wrong” bugs.

tsx
// Two sources of truth. They will disagree.
const [items, setItems] = useState([])
// updated... everywhere?
const [total, setTotal] = useState(0)

// One source of truth. It cannot disagree.
const total = items.reduce((n, i) => n + i.price * i.qty, 0)

The first version needs every mutation site to remember to update total. The bug is not that someone forgets once — it is that a new mutation site added in six months has no way of knowing it should.

Gotchas

  • The boundary is occasionally fuzzy — optimistic UI temporarily holds server-shaped data in client state, then reconciles. That’s fine; it’s the exception, handled by the server-state lib’s mutation API (Optimistic Updates and Rollback).
  • Auth/session token is borderline — usually client state (a value you hold) backed by a server check; keep the token out of localStorage if you can (see Frontend Security — Senior Interview Prep).
  • Form state is client state until submit; don’t sync every keystroke to a global store.
  • Don’t double-cache — if TanStack Query already caches the user, don’t also copy it into Redux “to be safe.” One source of truth.

Further reading

Interview angle 4

  • “What is the distinction that matters?” - server state is a cache of data someone else owns and can change without telling you; client state is UI state you own and that is never stale. They have different failure modes, so they want different tools.
  • “What goes wrong when you put API responses in Redux by hand?” - you end up reimplementing caching, deduplication, background refetch and invalidation, one reducer at a time, and the result is worse than the library that does it. Every “why is this data stale” bug traces back to that decision.
  • “So what is left for the client store?” - genuinely local UI state - a wizard step, a modal flag, a draft form - which is usually useState, Context, or a small Zustand store. Once server data moves out, most applications need far less client state than they have.
  • “Do you store derived state?” - no. Compute it from the source and memoize if profiling says to. Storing a derived value creates a second source of truth that can disagree with the first, and keeping them in sync is a bug factory.