What causes unnecessary re-renders in React, and how do you prevent them
Common causes: parent re-render cascading, unstable prop references (new object/array/function each render), context value changing every render, hot state in Context, missing memoization for expensive children. Fixes: keep prop references stable (useMemo/useCallback when memo is downstream), split context, lift hot state to a selector-based store, React.memo for genuinely heavy children. Profile before applying.
What "re-render" actually means
When a component renders, React calls its function and reconciles the returned JSX. Re-rendering itself is cheap; the downstream effects (large child trees, expensive computation, layout work) are what hurts.
Common causes
1. Parent re-render cascade
Any state change in a parent re-renders all children unless they're memoized. Usually fine — re-render of 10 components is trivial.
2. Unstable prop references
function Parent() {
return <Child config={{ size: 4 }} onClick={() => doThing()} />;
}Every render creates a new object + function. If Child is React.memo'd, the memo bails because props differ by reference. Fix:
const config = useMemo(() => ({ size: 4 }), []);
const onClick = useCallback(() => doThing(), []);3. Context value changes every render
<Ctx.Provider value={{ user, setUser }}> // new object each renderEvery consumer re-renders. Fix:
const value = useMemo(() => ({ user, setUser }), [user]);4. Hot state in Context
Context doesn't have selector subscriptions; any value change re-renders every consumer. Mouse position in Context = disaster. Move hot state to Zustand/Redux with selectors.
5. Inline definitions of memoized children
const X = memo(() => ...); // good — defined outside
function Parent() {
const X = memo(() => ...); // BAD — new component identity each render
}6. useEffect setting state every render
useEffect(() => { setX(compute()); }); // no deps → fires every render → loop7. Spread on every render
<Child {...props} extra={extra} /> // extra is fresh each renderWhen NOT to fix
If profiling doesn't show a hot path, memoizing adds cost (allocation, comparison) for no benefit. The reflex "useMemo everything" is an anti-pattern.
Profiling
React DevTools Profiler → "Why did this render?" shows whether it was parent re-render, state change, hook change, or props change. Highlight Updates renders to see what's flashing.
Fix patterns
| Symptom | Fix |
|---|---|
| Big child re-rendering on parent state | React.memo on child + stable prop refs |
| Context fan-out | Split context, or move hot state to selector store |
| Effect-driven re-render loop | Add deps; lift derivation out of state |
| Inline component defs | Move outside render |
Interview framing
"Re-renders aren't inherently bad — most are cheap. Real bottlenecks come from cascading into expensive subtrees: unstable prop references defeat memo, Context value changing every render fans out to every consumer, hot state in Context kills perf, inline component definitions break memo identity. The fix order: profile to find the actual hot path; stabilize prop references with useMemo/useCallback for the cases that matter; split context; move hot state to a selector-based store. Don't sprinkle memoization preemptively."
Follow-up questions
- •When does memoization hurt?
- •How would you debug a context fan-out?
- •Why does inline component definition break memo?
Common mistakes
- •useMemo/useCallback everywhere reflexively.
- •Context with hot state.
- •Effects with missing deps causing loops.
Performance considerations
- •Each memo costs allocation + comparison; only apply on confirmed hot paths.
Edge cases
- •Strict Mode double-render in dev.
- •Reconciliation across keys.
- •Memo + new object prop = always re-render.
Real-world examples
- •React docs profiling examples, Kent C. Dodds context blog post.