跪拜 Guibai
← All articles
React.js · Performance Optimization · Full Stack

Why React.memo Breaks When You Pass a Function (and How to Fix It)

By 烬羽 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Memoization is the most common React performance lever, but a single inline function silently disables it across an entire component tree. Understanding that memo, useCallback, and useMemo form a single interlocking mechanism prevents wasted renders that accumulate into perceptible lag.

Summary

A React component wrapped in memo still re-renders when a parent passes a callback, even if the callback's logic never changes. The culprit is JavaScript's reference semantics: every render produces a brand-new function object, and memo's shallow equality check sees a different prop. The fix is useCallback, which returns the same function reference across renders unless its declared dependencies change.

A controlled experiment shows the gap: a memoized child receiving a stable callback via useCallback renders once, while an identical memoized child receiving a raw arrow function re-renders on every parent update. After five parent renders, the unstable version has rendered six times to the stable version's one.

Blanket-wrapping every function in useCallback backfires. The hook carries memory and comparison overhead, and it delivers zero benefit unless the receiving component is already wrapped in memo. React's design deliberately avoids automatic memoization because shallow comparison itself has a cost, and correctness always takes priority over speculative optimization.

Takeaways
React.memo uses shallow comparison (===), so a new function reference counts as a changed prop even if the function body is identical.
useCallback stabilizes a function reference across renders; it only returns a new reference when its dependency array values change.
Passing a stable callback to a memoized component can cut re-renders by an order of magnitude in hot paths.
useCallback provides no performance benefit unless the receiving component is wrapped in React.memo.
useCallback itself has costs: memory for the cached reference and dependency array, plus per-render dependency checks.
React does not auto-memo because shallow comparison overhead can exceed re-rendering cost, and correctness comes before optimization.
Conclusions

The memo-useCallback pairing is less a performance feature and more a leak-plugging exercise: one missing useCallback anywhere in a prop chain silently disables memoization for every child below it.

React's refusal to auto-memoize is a deliberate trade-off that pushes measurement discipline onto developers. The framework optimizes for correctness and trusts tooling like the Profiler to surface real bottlenecks.

The mental model shift that matters is treating function identity as a first-class stability concern, on par with the data values themselves.

Concepts & terms
Shallow comparison
React.memo compares previous and next props using strict equality (===). For primitives like strings or numbers, this checks value equality. For objects, arrays, and functions, it checks reference identity — two objects with identical contents but different memory addresses are considered different.
useCallback
A React hook that caches a function definition between re-renders. It returns the same function reference unless values in its dependency array change. It does not make the function faster; it only stabilizes the reference for downstream memoization.
useMemo
A React hook that caches the result of a computation. Like useCallback (which is essentially useMemo for functions), it stabilizes object or array references so that memoized children do not re-render unnecessarily.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗