React.memo Is Not Working - How to Actually Stop Re-renders
5 min read
“memo checks one thing: same reference as last time? Inline objects and functions are never the same reference twice. So memo always says no - and the child always re-renders.”
You added React.memo. You can see it in the source. But the child still
re-renders on every parent update, every keystroke, every unrelated state change.
memo is not broken. It is working perfectly. The problem is what you are handing
it to compare.
Before React re-renders a memoized child, it runs a shallow equality check on every
prop using Object.is.
For each prop: is this the same reference as last render? If all props pass, skip
the re-render. If one prop has a new reference, re-render.
That is the entire algorithm. It does not read your objects deeply. It does not
compare their contents. It compares addresses.
This means: if you create an object or function inside the component body and pass
it as a prop, memo will fail on every render. Every render creates a new address.
Every comparison returns false.
export default function SettingsPanel() { const [query, setQuery] = useState(""); const config = { theme: "dark" }; // new object, new address, every render return <ThemeDisplay config={config} />;}
User types a character. SettingsPanel re-renders. const config = { theme: "dark" }
runs again. Same contents, different address.
Object.is(prevConfig, newConfig) - two addresses - returns false. Child
re-renders. Every keystroke. The theme never changed. The contents are identical.
memo does not know that.
useMemo runs the factory once and returns the same reference on every render.
Object.is returns true. memo holds. The child skips re-rendering when query
changes.
When the config depends on state, list those in the deps:
Functions created inside the component body have the exact same problem. A function
is an object allocation - new reference every render.
function handleAction() { // new reference every render alert("Action!");}return <ActionButton onClick={handleAction} />;
ActionButton receives a new onClick reference on every keystroke. memo fails
every time. The button re-renders even though the label never changed and the
handler does the same thing it always did.
Same reference every render. Object.is returns true. memo holds.
The Sensei's Hint
Quick check: are you passing an object or function as a prop to a memoized
component? If it is declared inline in the render body - not from useMemo or
useCallback - memo will not work for that prop, full stop.
You add useCallback and the child still re-renders. The instability is one level
up, in the dependency itself.
const searchConfig = { caseSensitive: false }; // new reference every renderconst handleApply = useCallback( (tag) => setActiveTag(searchConfig.caseSensitive ? tag : tag.toLowerCase()), [searchConfig], // searchConfig is new every render - so is handleApply);
useCallback is present. But searchConfig is its dependency, and searchConfig
is an inline object - recreated every render. useCallback sees its dep changed
and recreates handleApply. memo sees a new onApply prop. Child re-renders.
Every. Keystroke.
The fix is not to change useCallback. The fix is searchConfig.
If it never changes, move it to module scope:
const searchConfig = { caseSensitive: false }; // module scope - one address foreverexport default function TagSearch() { const handleApply = useCallback( (tag) => setActiveTag(searchConfig.caseSensitive ? tag : tag.toLowerCase()), [searchConfig], // now stable - handleApply created once );}
searchConfig at module scope is allocated when the module loads. Its reference
never changes. useCallback sees no change. handleApply stays stable. memo holds.
If searchConfig depends on state, use useMemo to stabilize it first, then
reference the memoized version in useCallback's deps.
Constraints
memo works only if every prop is reference-stable. One unstable prop breaks the
whole check - regardless of how many others are correctly memoized.
memo runs a comparison on every render. For components that do simple, fast
work, that comparison can cost more than just re-rendering.
Add memo when you have a confirmed problem - a child doing expensive work or
rendering a large subtree that is re-rendering more than it should. Use React
DevTools Profiler to confirm before adding it. Do not add it preemptively.
The useMemo and useCallback patterns here are useful beyond memo - they also
stabilize values used in dependency arrays and context values. Understanding
reference identity is one of the higher-leverage React concepts to have in your
head.
Three katas show this bug from different angles. "Memo Doesn't Hold" is the entry
point - one inline config object, one render counter that will not stop. "Child
Re-renders on Every Keystroke" is the same failure with a function prop. "Filter
Buttons Rerender on Every Search Keystroke" is the compound case where
useCallback alone is not enough - the dependency chain needs fixing first.