What React.memo actually checks
React.memo wraps a component so that, before re-rendering it, React does a shallow comparison of its new props against its previous props. If every prop is === to what it was last time, React skips re-rendering that component and reuses the previous output. "Shallow" is the key word — it compares object and function props by reference, not by deep value, which is exactly where the tool's biggest gotcha lives.
When developers actually run into this
Someone notices a child component re-rendering more than expected, wraps it in React.memo, and sees literally no change — the child keeps re-rendering every time the parent does. The instinct is to assume memo doesn't work or isn't worth using, when the actual cause is almost always that the parent passes a prop whose reference changes on every render, even if its contents are logically the same.
The problem
const Child = React.memo(({ onClick, style }) => {
console.log('Child rendered');
return <button onClick={onClick} style={style}>Go</button>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Increment: {count}</button>
<Child
onClick={() => console.log('clicked')}
style={{ color: 'blue' }}
/>
</>
);
}
// Click "Increment" five times.
// logs: "Child rendered" — five times, once per Parent re-render
// The memo comparison never passes, because onClick and style are BOTH new
// object/function references on every single Parent render, regardless of count
The fix
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => console.log('clicked'), []);
const childStyle = useMemo(() => ({ color: 'blue' }), []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Increment: {count}</button>
<Child onClick={handleClick} style={childStyle} />
</>
);
}
// Click "Increment" five times.
// logs: "Child rendered" — once, on the initial mount only
// useCallback and useMemo return the SAME reference across renders (given empty deps),
// so memo's shallow comparison now actually passes
Why this happens (the deeper reason)
JavaScript's === for objects and functions compares identity, not structural equality — two objects with identical contents are still !== if they're different instances in memory, and an inline arrow function or object literal creates a brand-new instance on every render, no matter how identical it looks to the previous one. React.memo's shallow comparison inherits this exact behavior, because doing a deep comparison of every prop on every render would often cost more than the re-render it's trying to avoid — so memo intentionally stays cheap and reference-based, which means it's only as effective as the stability of the references being passed into it. useCallback and useMemo exist specifically to produce a stable reference across renders (as long as their dependencies don't change), which is the missing piece that makes memo actually able to detect "these props are genuinely the same."
Top comments (0)