Back to React
React
easy
mid

How do you design React components for 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.

5 min read·~15 min to think through

"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):

jsx
const Row = React.memo(function Row({ item }) { ... });

React.memo skips re-render when props are referentially equal.

3. Stable callback / object props

jsx
// 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:

jsx
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:

jsx
<ThemeContext.Provider>
  <UserContext.Provider>
    <CartContext.Provider>...</CartContext.Provider>
  </UserContext.Provider>
</ThemeContext.Provider>

Or use stable context values:

jsx
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:

jsx
// Zustand
const cartCount = useCart((s) => s.items.length);   // only re-renders when count changes

6. Avoid inline objects/arrays in JSX

jsx
<List items={items.filter(active)} />   // new array every render

useMemo it or compute outside if reused.

7. Functional setters

Prevent unnecessary "set to same value" re-renders:

jsx
setCount((c) => c + 1);     // bail-out if same value

React 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:

  1. ✅ Profile with DevTools.
  2. ✅ Move state down (colocate).
  3. ✅ Memo leaf components.
  4. ✅ Stabilize props (useCallback / useMemo).
  5. ✅ 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.

Senior engineer discussion

Seniors colocate, profile, split contexts, and reach for external stores deliberately — they don't useMemo everything. They distinguish hot from cold state.

Related questions