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
- TanStack Query (the canonical server-state tool): ../11_apis_data_fetching/02_tanstack_query.md
- RTK Query (Redux’s server-state layer): redux/rtk_query.md
- Choosing a client-state library: choosing_a_state_library.md
- Optimistic updates (the fuzzy boundary): ../11_apis_data_fetching/03_optimistic_updates.md
Further reading
- TanStack Query — “Does this replace Redux?”: https://tanstack.com/query/latest/docs/framework/react/guides/does-this-replace-client-state
- Kent C. Dodds — “Application State Management with React”: https://kentcdodds.com/blog/application-state-management-with-react