Back to the Library

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.”

In This Post

The One Thing memo ChecksWhy Inline Objects Break memoFix Objects With useMemoWhy Inline Functions Break memo TooFix Functions With useCallbackWhen useCallback Still Does Not HelpWhen memo Is Blocking Updates It Should AllowWhen Not to Add memo at AllPractice These Patterns

Practice This Pattern

Blue Belt

Memo Doesn't Hold

A memoized theme display whose render counter climbs on every keystroke - an inline config object creates a new reference each render, making memo's check always fail.

+25 KI
Enter the Dojo
Blue Belt

Child Re-renders on Every Keystroke

A memoized action button that re-renders on every search keystroke - a plain function prop is recreated each render, defeating memo despite the label never changing.

+25 KI
Enter the Dojo
Blue Belt

Filter Buttons Rerender on Every Search Keystroke

Two memoized filter buttons that re-render on every keystroke - an inline object breaks useCallback, which breaks memo, two levels down the chain.

+25 KI
Enter the Dojo
BugDojo
BlogFAQ

© 2026. Carved in code.

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.

The One Thing memo Checks

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.

Why Inline Objects Break memo

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.

Fix Objects With useMemo

export default function SettingsPanel() {
  const [query, setQuery] = useState("");
  const config = useMemo(() => ({ theme: "dark" }), []);
 
  return <ThemeDisplay config={config} />;
}

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:

const config = useMemo(() => ({ theme, fontSize }), [theme, fontSize]);

The child now only re-renders when theme or fontSize actually changes.

Why Inline Functions Break memo Too

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.

Fix Functions With useCallback

const handleAction = useCallback(() => {
  alert("Action!");
}, []);

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.

When useCallback Still Does Not Help

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 render
 
const 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 forever
 
export 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.

When memo Is Blocking Updates It Should Allow

The opposite problem: a memoized child that receives no props never re-renders, even when it should.

If the child reads a module-level variable directly, memo has nothing to compare and will block every re-render:

let externalScore = 0;
 
const ScoreDisplay = memo(function ScoreDisplay() {
  return <p>Score: {externalScore}</p>; // reads outside React's system
});

Parent increments externalScore. memo sees no prop changes. Child never re-renders. Score stays at 0 on screen forever.

Fix: pass the value as an explicit prop. Now memo can compare it.

const ScoreDisplay = memo(function ScoreDisplay({ score }) {
  return <p>Score: {score}</p>;
});
 
<ScoreDisplay score={externalScore} />

When score changes, memo sees it and allows the re-render.

When Not to Add memo at All

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.

Practice These Patterns

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.