State Management Patterns
Date: 2026-08-19
Four mechanisms, and the differences that matter are how a change propagates and how much re-renders when it does. Everything else — action creators, immutability rules, devtools — is convention layered on top of one of the four.
State management patterns are the mechanisms for holding client-owned state outside the component tree and getting changes to whoever is reading them. Which state belongs here at all is Component State vs Application State; this note assumes that question is already answered.
The four mechanisms
Single store with reducers. One object holds everything; changes are described as plain values (actions) and applied by a pure function. Components subscribe to slices.
dispatch({type: 'cart/lineAdded', payload: {variantId, quantity: 1}});
// reducer: (state, action) => newState — pure, replayable, serialisableThe payoff isn’t the store, it’s the action log: every change is a recorded value, so time-travel debugging, replay from a bug report and audit trails all fall out for free. The cost is ceremony per change, and the strong pull towards putting server data in there too.
Context / provider injection. The framework’s own dependency injection: a value placed at a node in the tree, readable by any descendant without threading props.
Context is a transport, not a store, and confusing the two is among the most common performance bugs in React apps. When the value changes, every consumer re-renders — every one, regardless of which field of the value it actually reads. The usual trigger is an unmemoised object literal as the value, which is a new reference on every render of the provider’s parent, so consumers re-render even when nothing in it changed. Correct for values that rarely change (theme, locale, the authenticated user); wrong for anything that changes per keystroke.
Atoms. State split into many small independent units, composed into derived values by a graph. A component subscribes to individual atoms, so a change reaches only its actual readers — Signals with a component-facing API.
Proxied mutable state. You mutate a plain-looking object and the proxy records which properties each component read, notifying only those. Ergonomically the nicest and structurally the same as atoms.
Which to reach for
| Propagation | Re-renders | Best for | Fails at | |
|---|---|---|---|---|
| Single store | Subscribe to slices | Selector-scoped | Auditable, complex transitions | Boilerplate; tempts you to cache server data |
| Context | To every consumer | All of them, whichever field changed | Rarely-changing ambient values | Frequent updates — re-render storms |
| Atoms | Per-atom graph | Only actual readers | Many independent pieces | Diffuse state, harder to see the whole |
| Proxies | Per-property | Only readers of that property | Ergonomics, less ceremony | Magic; hard to trace what changed |
Start with none of them. Local state plus the URL plus a fetching layer covers most applications entirely — see Data Fetching Patterns.
What actually goes wrong
Context re-render storms. A provider holding {user, cart, theme, isModalOpen} re-renders every consumer whenever any of the four changes. The fix is to split providers by change frequency, not by topic — Long Tasks and Blocking explains why this shows up as input lag rather than as a rendering complaint.
The store as an application-wide cache. Covered in Component State vs Application State, and worth repeating because it survives every refactor: a store has no concept of staleness, so you write invalidation by hand forever.
Derived state stored instead of computed. Writing total into the store alongside items creates two sources of truth that drift the moment one path updates one and not the other. Derive it — a computed value can’t be inconsistent because it doesn’t exist between reads.
Selectors returning fresh objects. A selector ending in .map() or {...} produces a new reference every run, which reads as “changed” to a reference-equality check, so the component re-renders on every store update regardless of subject. This is the one that gets diagnosed as “the store is slow”.
The honest framing
Every one of these is a subscription mechanism with a different granularity, exactly as in Reactivity. Choosing between them is choosing how much re-renders when something changes, and how much ceremony you’ll accept in exchange for being able to see what changed.