frontend / state managers / server_vs_client_state.md

Server State vs Client State

4 min read source

Server State vs Client State

TL;DR

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.

Interview Q&A

Q: Define server state and client state.

A:

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.

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

A: 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.

Q: What does a server-state library actually give you?

A: Using TanStack Query as the example:

function Profile({ id }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ["user", id],
    queryFn: () => fetchUser(id),
    staleTime: 30_000,        // serve from cache for 30s before refetching
  });
  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 ../11_apis_data_fetching/02_tanstack_query.md.

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

A: 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.md).

Q: Where do RTK Query and TanStack Query fit?

A: Both are server-state libraries. RTK Query (redux/rtk_query.md) 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.

Q: What about derived state — store it or compute it?

A: Don’t store what you can derive. totalPrice = items.reduce(...), filteredList = list.filter(...) should be computed at render (memoize with useMemo/selectors 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.

Gotchas / edge cases

  • 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 (../11_apis_data_fetching/03_optimistic_updates.md).
  • 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 ../17_security/).
  • 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.

What a senior is expected to say

  • “Server state is a cache of remote data; client state is UI state I own. They need different tools.”
  • “Putting API responses in Redux by hand reimplements caching, dedup, and refetch — I use TanStack Query or RTK Query for that.”
  • “Client store is then only for genuinely-local UI state, often just useState/Context or a small Zustand store.”
  • “I don’t store derived state — I compute it and memoize if needed.”

Cross-references

Further reading