A 32-minute working session on the question that quietly decides your app's architecture: where does each piece of state live? From React's built-ins to global stores and server-state caches — and the honest trade-offs between them.
Before reaching for any library, ask one question of every value: do you own it, or is it a copy of something that lives on a server? Almost every state-management mistake comes from blurring those two — and most "we need Redux" conversations evaporate once you separate them.
Same word, two different problems. Client state is owned and instant; server state is a borrowed copy you must keep fresh.
Most of this deck is really one lesson: stop hand-rolling a cache out of useState + useEffect, and stop putting owned UI state on a server.
Most apps need far less global state than they think. Reach for a library only when these three genuinely run out of room. For the mechanics of hooks themselves, see Modern React — here we focus on where each one fits.
The default. A piece of state owned by a single component (and maybe passed down a level or two). When two updates depend on the previous value, use the functional form.
When several actions change related state in structured ways, a reducer keeps the logic in one testable function instead of five scattered setState calls.
Context is not a state manager — it's a transport. It moves a value down the tree without threading props through every layer. It does no caching and no selective updates.
Any change to a context value re-renders every consumer — even ones that only read an unrelated field. Context has no built-in selectors.
Two things make Context bite. First, putting a fast-changing value (mouse position, a growing list) next to slow-changing ones in a single context. Second, passing a fresh object as the value every render — a new identity always counts as a change.
Global stores solve the two problems Context can't: they live outside the component tree, and components subscribe to a slice — so a change re-renders only the components that read that slice. That selector model is the real reason to use one.
Add an item and only Count re-renders — Avatar and Header read other slices and stay idle.
All four solve sharing. They differ in mental model and ceremony. One-line pro, con, and when each fits.
One create call returns a hook; no provider, no boilerplate. Selectors are built in. The pragmatic default for most apps in 2026.
The official, modern Redux. Slices generate actions and reducers for you; strong conventions, time-travel DevTools, and a middleware ecosystem.
State is built from tiny atom primitives that compose; derived atoms recompute only when their inputs change. Granular by design, with a React-native feel.
A signal is a value that notifies its readers directly, skipping component-level diffing. Native in SolidJS and Angular; in React via Preact Signals. The future direction for granular updates.
The honest default: if Context plus a little lifting still works, you don't need any of these yet. When you do, Zustand is the smallest step up for most teams.
The moment you fetch, you hold a copy that can be wrong. A server-state library makes that copy honest: it dedupes requests, caches by key, serves instantly while refetching in the background, and gives you a clear way to invalidate. See also Caching & CDNs for the caching fundamentals and API Design for the endpoints behind these keys.
The cache answers instantly, refetches behind the scenes, then re-renders with fresh data. The query key is the identity of each cache entry.
These are caches, not stores. Pick by how much you need and what you're already running.
Caching, deduping, background refetch, retries, pagination, optimistic updates, and first-class DevTools. Framework bindings for React, Vue, Svelte, Solid and more.
staleTime, cache lifetimes.From Vercel, named after stale-while-revalidate. A tinyuseSWR(key, fetcher) covers fetching, caching, and revalidation with very little API.
Part of Redux Toolkit. You declare endpoints; it generates hooks and stores the cache in your Redux store, with tag-based invalidation and optional code generation from your API schema.
How to choose: already on RTK → RTK Query. Lightweight, mostly reads → SWR. Anything richer → TanStack Query. All three beat a hand-rolled useEffect fetch.
Every value you store is a value that can drift out of sync. Two habits prevent most bugs: derive anything you can compute from existing state, and keep a single source of truth for each entity instead of scattering copies.
Store items; everything else is a function of it. Reach for useMemo only when the computation is genuinely expensive — correctness first, then performance.
When the same entity appears in many places, don't embed copies of it. Store each entity once, keyed by id, and reference it. Update one record and every view updates — there are no stale duplicates to chase. It's how a database thinks, and why tools like Redux ship an entity adapter.
Rename Ada once in users[9] and both posts reflect it — no duplicate to forget.
One stored array; total, count, and filtered views are all computed on render — nothing to keep in sync.
useState: can I compute this instead? If yes, don't store it.Before adding a store for filters or a wizard, check whether the state belongs in the URL or stays inside a form. Both are real state containers you already have — and using them deletes whole categories of bugs.
Put filters and pagination in the query string and the link is the state — no store, and every view is shareable.
A form draft is ephemeral, local client state — it rarely belongs in a global store. The real choice is controlled (React owns each keystroke) vs uncontrolled (the DOM owns the value, React reads it on submit). Uncontrolled forms re-render far less, which is why form libraries lean that way.
The form library owns the draft. It reaches a store or a server only when the user hits submit.
There's no single "state management library" — there are categories, and most apps use a couple together. Route each piece of state by ownership and the choice mostly makes itself.
Server, URL, and a single subtree absorb most state. A global client store is the last resort, not the first.
useState/useReducer, lift only as far as needed.Redux Toolkit is still excellent for large teams that want enforced structure and audit-friendly logic. But a huge share of historical Redux was really server state — and that job now belongs to a query cache. Separate the two first; many apps find they need very little global client state at all.
useState + useEffect.useState → lift → Context → a store with selectors — only as the need appears.Five quick questions on the client/server split, Context, stores, caching, and where state belongs — instant feedback, no sign-in.
Navigate with ← → or scroll · back to library