Build a Predictable re-renders
Predictable re-renders: colocate state so changes have a narrow blast radius; memoize children with React.memo + stable callback identity (useCallback/useMemo for props); split contexts so a frequently-changing slice doesn't re-render unrelated consumers; for hot state, use external stores (Zustand) with selector-based subscriptions; profile with DevTools to verify before optimizing.
"Predictable re-renders" is the goal — every render is intentional, traceable, and minimal. Achieving it is mostly about where state lives and what props are stable.
1. Colocate state
The default fix: put state next to where it's used (see [[what-is-state-colocation-and-why-does-it-matter]]). Local state has a narrow blast radius — only that component and its children re-render.
2. Memo children when state can't be local
When state must live higher (multiple siblings need it):
const Row = React.memo(function Row({ item }) { ... });React.memo skips re-render when props are referentially equal.
3. Stable callback / object props
// PROBLEM
<Row onSelect={(id) => setActive(id)} /> // new fn every render → memo bypassed
// FIX
const onSelect = useCallback((id) => setActive(id), []);
<Row onSelect={onSelect} />Same for objects:
const style = useMemo(() => ({ color: theme }), [theme]);
<Row style={style} />4. Split contexts
A single AppContext with {theme, user, cart, modal} re-renders every consumer when any of those changes. Split:
<ThemeContext.Provider>
<UserContext.Provider>
<CartContext.Provider>...</CartContext.Provider>
</UserContext.Provider>
</ThemeContext.Provider>Or use stable context values:
const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
<ThemeContext.Provider value={themeValue}>...5. Use external stores for hot state
Context re-renders every consumer on value change. For hot, fine-grained state, an external store with selector-based subscriptions is better:
// Zustand
const cartCount = useCart((s) => s.items.length); // only re-renders when count changes6. Avoid inline objects/arrays in JSX
<List items={items.filter(active)} /> // new array every renderuseMemo it or compute outside if reused.
7. Functional setters
Prevent unnecessary "set to same value" re-renders:
setCount((c) => c + 1); // bail-out if same valueReact skips the render if the new value is Object.is-equal — but the functional form ensures correctness across batched updates.
8. Don't fight reconciliation
Same component type + same key → React updates in place. Reusing components is the whole point. Don't memoize everything; memoize where measurement says it matters.
9. React DevTools Profiler
When in doubt, profile:
- Record an interaction.
- See which components rendered, how long, and why.
- Optimize the biggest cost.
Pre-optimization based on intuition is often wrong.
10. Hot vs cold state
- Cold state (rare changes): context is fine.
- Hot state (per-keystroke, per-scroll, per-frame): external store, refs, or local state in a leaf.
Common anti-patterns
- Wrapping every callback in useCallback with no consumer that needs identity stability — pure overhead.
- Memoizing every value including primitives — overhead.
- Big monolithic context — re-renders the world.
- State at the top with no memoization → predictable, but wide re-renders.
Concrete checklist
Before assuming you have a re-render problem:
- ✅ Profile with DevTools.
- ✅ Move state down (colocate).
- ✅ Memo leaf components.
- ✅ Stabilize props (useCallback / useMemo).
- ✅ Split contexts or move to external store.
In that order. Skipping straight to "useMemo everything" doesn't help.
Interview framing
"Predictable re-renders come from intentional state placement plus stable identity at component boundaries. Default: colocate state next to where it's used so changes have narrow scope. When state must live higher, memo children with React.memo and stabilize their props with useCallback/useMemo — otherwise memo is bypassed by fresh references. Split a big context into focused ones, or stabilize the value object, so a frequently-changing slice doesn't re-render every consumer. For hot state (per-keystroke, per-scroll), reach for an external store like Zustand with selector-based subscriptions — context's all-consumers-re-render model is wrong shape. Profile first; don't optimize on intuition. The order is colocate → memo where measurement shows benefit → split contexts → external stores for truly hot state."
Follow-up questions
- •Why does context re-render every consumer?
- •When does React.memo help and when is it overhead?
- •What's the difference between hot and cold state?
- •How do you decide when to reach for an external store?
Common mistakes
- •useMemo / useCallback everywhere preemptively.
- •Monolithic context with frequently-changing slices.
- •Memoizing components while passing fresh-identity props.
- •Optimizing without profiling.
Performance considerations
- •Colocation is free. memo+stable props has a cost; net win only when measurement confirms.
Edge cases
- •Deeply nested trees where prop drilling is the bigger cost than context.
- •Hot state with infrequent consumers — context with bail-out can work.
- •useTransition for non-urgent updates.
Real-world examples
- •Zustand selectors for hot state; Jotai atoms.
- •react-redux with selector + shallowEqual.