Frontend / State managers / server_vs_client_state.md

Server State vs Client State

Updated 4 min read source
On this page6
  1. TL;DR
  2. In depth
  3. Gotchas / edge cases
  4. What a senior is expected to say
  5. Cross-references
  6. Further reading

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.

In depth

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 — Cache Keys, Invalidation, Prefetch.

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 (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.

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 = useMemo(
  () => items.reduce((n, i) => n + i.price * i.qty, 0),
  [items],
)

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 / 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 (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.

What a senior is expected to say 4

  • “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