React State Management Choices
Keep state local and atomic; use useReducer when transitions get complex, Context for low-churn values, and a selector store like Zustand or Redux when many components read fast-changing shared state.
🌱 A rough idea, planted recently. Expect rough edges — it will grow.
Keep state as local and as atomic as possible. Reach for useReducer when transitions get complicated, Context for
values that rarely change, and a selector store (Zustand, Redux) when many components read fast-changing shared
state.
The state flow
User action, then an event, a state update, a re-render, and the UI shows the new state. Every choice below is really about how much of the tree re-renders at the state-update step.
Atomic state
State that changes together should update together: one { isLoading, quoteValue } object (or a reducer) instead of
two useStates set one after the other. One update, one re-render, and no impossible in-between states.
Functional updates read the latest state and drop a dependency:
setUser((prev) => ({ ...prev, profilePicture: newPic })); // no `user` in the deps
useReducer
- All the transitions live in one place (
dispatch({ type: "pageChanged", page })), so components get simpler and fewer props churn. - It pairs with Context to avoid prop drilling, and the shape scales up to a global store; Redux is the same idea.
- A gotcha I’ve hit building a paginated table: derived state, like the current page of rows, has to be recomputed from reducer state. If the reducer mutates and returns the same object, or the derived value isn’t recalculated, nothing re-renders. Always return new objects.
Context vs Redux vs Zustand
| Use for | Watch out | |
|---|---|---|
| Context | Low-churn values: theme, the signed-in user, dependency injection; small subtrees | Every consumer re-renders on any change |
| Redux Toolkit | Large, complex app state with strict patterns and great tooling | Boilerplate; selectors keep it fast in very dynamic apps |
| Zustand | My default for shared client state: small API, selector subscriptions, usable outside React | Needs discipline about store boundaries |
| TanStack Query | Server state (cache, refetch), which isn’t client state at all | Don’t copy server data into a store |
The cost of centralised state: one big store or context means unrelated changes can re-render parts of the UI that don’t care. Narrow selectors and splitting stores fix it. For something like a live price blotter, use a selector store with transient updates for the hottest values (Zustand Transient Updates).
Related
- How to find the re-renders in the first place: Finding React Re-render and Memory Leaks.
- A longer look back at how my thinking on this has changed: State Management in React, Revisited.
- Where data fetching fits: React Data Fetching Patterns.
- #react
- #state-management
- #zustand
- #redux
- #context
↩ Linked from (1)
…at Sheet). Avoid parent-driven re-renders. Keep state low and use selector stores (Zustand Transient Updates, React State Management Choices). Stable list keys. Never the index for a dynamic list. Virtualisation (windowi…
↪ Links to (4)
Related by topic
- Growing
AG Grid Performance Patterns
Real-time UIs - Growingcase study
Building a Graph Component Library
Frontend craft - Growing
Graph Visualisation Libraries
Real-time UIs - Growing
Rust and WebAssembly: Benchmark Before You Bet
Frontend craft