codejump

AcademyReact

React.memo and Unnecessary Re-renders

A component re-renders whenever its parent does, props unchanged or not — usually fine, and React.memo is the opt-out for when it measurably is not.

4 min readUpdated

3 exercises

Waiting at the end of this page.

The default: children re-render with their parent

React's baseline behavior surprises people the first time they see it: when a component re-renders, every child it returns re-renders too, whether or not that child's own props actually changed.

JSX
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      <ExpensiveChart data={staticData} />
    </>
  );
}

Clicking the button re-renders Parent, which re-renders ExpensiveChart too, even though staticData never changed and never will. Nothing is broken here — this is React working as specified.

Why that default is fine almost all the time

"Re-render" does not mean "repaint the DOM." A re-render calls the component function again, builds a fresh element tree, and hands it to reconciliation — which diffs it against the last tree and only touches the real DOM where something actually differs. For ExpensiveChart above, if its output does not depend on anything that changed, reconciliation bails out at the diff step and nothing is written to the DOM. The wasted work is calling the function and building the throwaway object tree, which for the overwhelming majority of components costs microseconds.

This is why "prevent re-renders" is the wrong starting instinct. React was built assuming components re-render often and cheaply; treating every re-render as a bug to eliminate turns a non-problem into a maintenance burden, usually for a performance win nobody would ever measure.

What React.memo actually changes

React.memo wraps a component and adds one check before it runs: compare this render's props to last render's props, shallowly (Object.is per key, one level deep — not a deep equality check), and skip calling the component entirely if they are all equal.

JSX
const ExpensiveChart = React.memo(function ExpensiveChart({ data }) {
  return renderChart(data); // assume this is genuinely costly
});

Now when Parent re-renders and data is the same reference as last time, ExpensiveChart is skipped — React reuses the element tree from the previous render for that subtree without calling the function again. That is the entire feature: a shallow-equality gate in front of a component call.

Shallow equality is the whole catch

"Shallow" is doing a lot of work in that sentence, and it is where React.memo quietly stops helping. An object, array, or function created fresh inside the render is a new reference every time — even when its contents are identical to last render's — and Object.is sees two different references as unequal:

JSX
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      {/* New array literal every render — React.memo can never see this as "unchanged" */}
      <ExpensiveChart data={[1, 2, 3]} />
    </>
  );
}

[1, 2, 3] is a brand-new array on every call to Parent, regardless of whether the numbers ever change. React.memo's shallow comparison correctly reports "the data prop changed" — it did, by reference — and re-renders ExpensiveChart every single time, making the memo wrapper pure overhead: one extra comparison per render, for a skip that never happens. The same trap catches inline object literals (style={{ color: 'red' }}) and inline arrow functions (onClick={() => doThing()}) passed as props to a memoized child — each is a fresh reference, so memoization never triggers unless the value crossing the boundary was itself produced by useMemo or useCallback upstream.

When it is actually worth reaching for

Worth memoizing when BOTH are true:
  1. The component is expensive to render (measured, not assumed)
  2. It re-renders often with props that are frequently unchanged

A component that renders in under a millisecond gains nothing from skipping that millisecond — the comparison itself costs something, so a cheap component wrapped in React.memo can end up marginally slower, not faster. The components worth wrapping are the ones you can point to: a chart library render, a large virtualized list, anything doing real computation in its own body — combined with a parent that re-renders on unrelated state changes (a keystroke in a sibling input, a websocket tick) while this component's own props stay put.

The practical order of operations: build the feature without React.memo first. Reach for it only after the React DevTools profiler shows a specific component re-rendering often for no prop-related reason, and only after confirming the props it receives are stable (or wrapping them in useMemo/useCallback upstream so the shallow comparison can actually succeed).

Now practice it

2 challenges · 1 quiz

Reading this page is the cheap half. These are the exercises that make you use it.