Why React.memo Breaks When You Pass a Function (and How to Fix It)
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.
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.
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.