AniUI Academy

React.memo

Opting a component out of the default "parent re-renders, so I re-render too" behavior — how the shallow prop comparison works, and how this optimization backfires when misapplied.

8 min read

The previous lesson established the default: a parent re-rendering re-renders every child, regardless of whether that child's props changed. React.memo is the explicit, opt-in way to change that for a specific component.

The basic shape

import { memo } from "react";
 
function ExpensiveList({ items }) {
  console.log("ExpensiveList rendering");
  return (
    <ul>
      {items.map((item) => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}
 
export default memo(ExpensiveList);

Wrapping a component in memo doesn't change anything about how the component itself works — it changes what happens before it's asked to render again. When ExpensiveList's parent re-renders, React first compares the new items prop to the previous one; if they're equal, it skips calling ExpensiveList again entirely and reuses the last rendered output.

The comparison is shallow

"Equal" here means a shallow comparison: primitives (strings, numbers, booleans) are compared by value, but objects, arrays, and functions are compared by reference — the same Object.is-style comparison the dependency array lesson covered.

function Parent() {
  const [count, setCount] = useState(0);
 
  // A new array, created fresh on every render of Parent
  const items = [{ id: 1, name: "Task" }];
 
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <MemoizedList items={items} />
    </div>
  );
}

Even though items looks identical every render, it's a new array created each time Parent's function runs — so MemoizedList's shallow comparison sees a different reference and re-renders anyway, memo or not. This is the single most common way React.memo quietly fails to help: memoizing a component doesn't do anything if the props it receives are freshly created every render regardless.

What actually needs to be true for memo to help

For React.memo to skip a re-render, the props passed to it need to actually be the same reference (for non-primitives) across the parent's renders when nothing relevant changed. That usually means the parent itself needs useMemo (for objects/arrays) or useCallback (for functions) around whatever it passes down — the next lesson covers both. React.memo and useMemo/useCallback are typically a package deal: memoizing a component without also stabilizing the references of what's passed into it often accomplishes nothing.

When it's actually worth reaching for

React.memo pays off specifically when: a component's own render is genuinely expensive (a large list, complex calculations, heavy child trees) and its parent re-renders often for reasons unrelated to this component's props. Both conditions matter — a cheap component re-rendering often is fine to just let re-render; an expensive component whose props change on almost every parent render gains nothing from memo, since the comparison will almost always conclude "re-render anyway," and now every render also pays for a comparison that never once paid off.

// A reasonable candidate: heavy render, and re-renders driven by unrelated
// state elsewhere in a large parent component
const HeavyChart = memo(function HeavyChart({ dataset }) {
  return renderComplexVisualization(dataset); // genuinely expensive
});

Try it yourself

Toggle the unrelated state and watch the console — the memoized child is skipped when its own props haven't changed, even though its parent re-renders:

Try it yourself
Loading playground...

What to remember

  • React.memo skips re-rendering a component if its props are shallowly equal to last time — an opt-in exception to the default "parent renders, child follows" behavior.
  • The comparison is shallow: a new object, array, or function reference counts as changed, even with identical contents.
  • Memoizing a component without also stabilizing the references of what's passed into it (via useMemo/useCallback in the parent) often skips nothing.
  • It pays off for genuinely expensive renders whose parent re-renders for unrelated reasons — not as a default wrapper for every component.

Check yourself

4 questions · pass 3/4 to unlock useMemo and useCallback

up to 50
  1. 1.What does wrapping a component in React.memo actually change?

  2. 2.What kind of comparison does React.memo do on props by default?

  3. 3.A memoized component still receives a fresh inline arrow function as a prop on every parent render (e.g. onSave={() => save(id)}). What happens?

  4. 4.Why isn't wrapping every component in React.memo a good default strategy?

4 left to answer