Frontend
medium
mid
How do you manage state in a complex app to avoid unnecessary re-renders
Classify state and place it well (server-cache lib for server state, colocate local state, selector stores for global), keep state low and narrow, split contexts, stabilize references, memoize selectively, and measure with the Profiler. Structure beats sprinkling memo everywhere.
7 min read·~12 min to think through
Avoiding unnecessary re-renders in a complex app is mostly a state architecture problem, not a memoization problem. Memoization patches symptoms; good state placement removes the cause.
1. Classify state and place it correctly
- Server state → React Query / SWR. Caching, dedup, and granular subscriptions mean components only re-render when their data changes — not when some unrelated fetch resolves.
- Local UI state →
useStatein the component that uses it. Colocate — state high in the tree re-renders a big subtree on every change. - Global client state → a selector-based store (Zustand, Redux +
useSelector, Jotai). Components subscribe to slices; changing one slice doesn't re-render subscribers of others.
2. Keep state low and narrow
- Colocation — push state down to the smallest subtree that needs it. The single most effective fix.
- Split state — don't bundle unrelated values into one object/context; a change to one field shouldn't re-render consumers of another.
- Lift only as far as necessary — most "global state" is state lifted too far.
3. Context: use it carefully
- Every consumer re-renders when the context value changes.
- Split contexts by update frequency — fast-changing values in their own context, or out of context entirely.
- Separate state context from dispatch/setters context (setters are stable).
- Memoize the context
valueobject. - For hot values, prefer a selector store over context.
4. Stabilize references
- New object/array/function literals each render break
React.memoand bloat effect deps. useMemo/useCallbackto keep props passed to memoized children stable.React.memoon components that re-render often via parents but whose props rarely change.- Pass content via
childrenso a parent's state change doesn't re-render the passed-in subtree.
5. Derive, don't duplicate
- Compute derived values (
useMemo, store selectors) instead of storing them — duplicated state drifts and triggers extra renders to "sync."
6. Measure — don't guess
- React DevTools Profiler + "highlight updates" +
why-did-you-renderto find the actual hot spots. Optimize those; ignore cheap re-renders. - Memoization has a cost — apply it where the profiler shows a frequent, expensive re-render.
- React Compiler (when adopted) auto-memoizes, shifting effort back toward good state structure.
Summary
Structure first (classify state, colocate, use selector stores, split contexts), stabilize references where it matters, derive instead of store, and measure to target the real hot spots. Sprinkling memo everywhere without that structure just adds cost and noise.
Follow-up questions
- •Why is colocating state more effective than adding React.memo?
- •How do selector-based stores avoid unnecessary re-renders?
- •How do you stop a Context from re-rendering all its consumers?
- •How do you decide where memoization is actually worth it?
Common mistakes
- •Sprinkling React.memo/useMemo everywhere without measuring.
- •Keeping state high in the tree when it could be colocated.
- •One giant context (or store object) re-rendering everything on any change.
- •Passing fresh object/function literals to memoized children.
- •Storing derived state and re-rendering to keep it 'in sync'.
Performance considerations
- •Re-renders are usually cheap; the cost is in frequent re-renders of expensive subtrees. Selector stores and colocation bound the blast radius structurally. Memoization trades comparison + memory for skipped renders — worth it only at real hot spots the Profiler confirms.
Edge cases
- •A memoized component still re-rendering because of unstable children.
- •Server data updates triggering unrelated component re-renders.
- •High-frequency values (scroll, cursor) in shared state.
- •Lists where every row re-renders on any change.
Real-world examples
- •Moving a search input's state out of a page-level store into the input fixed a grid re-rendering per keystroke.
- •Splitting a theme+liveData context into two so live updates stop re-rendering theme consumers.
Senior engineer discussion
Seniors treat this as state architecture: classify state, colocate, use selector stores, split contexts by update frequency, derive instead of store — then stabilize references and memoize only at profiler-confirmed hot spots. They note memoization has a cost and that React Compiler is shifting the emphasis back to good structure.
Related questions
Frontend
Medium
6 min
Frontend
Medium
7 min
Frontend
Easy
6 min