Skip to content
QSWEQB
mediumConceptCodingMidSeniorStaff

You wrapped the component in React.memo and it still re-renders on every keystroke. Why?

memo does a shallow comparison of props, so any prop recreated during the parent's render - an object literal, an inline callback, JSX passed as children - fails it every time. Context updates and state owned inside the component bypass the comparison entirely.

3 min readUpdated 2026-07-26

What the interviewer is scoring

  • Does the candidate name shallow reference comparison as the rule rather than saying memo "caches the component"
  • Whether they can point at which specific prop is failing the comparison instead of adding useCallback everywhere
  • That they know context and internal state changes re-render a memoised component regardless
  • Whether they separate re-rendering from DOM mutation, and can say which one the user pays for
  • Does the candidate check with the profiler before and after, rather than asserting the fix worked

Answer

What the comparison actually does

memo wraps a component so that before re-rendering it, React compares the incoming props with the previous ones using a shallow equality check: same set of keys, and each value equal by Object.is. If every prop passes, React skips rendering that subtree and reuses the previous output. That is the entire contract, and it means the effectiveness of memo is decided not inside the memoised component but in the parent that renders it.

A parent's render function runs top to bottom on every update, and every object literal, array literal, arrow function and JSX element it evaluates is a fresh value with a fresh identity. Pass any of them down and the comparison fails, memo re-renders the child, and you have added a comparison cost for no benefit.

const Row = memo(function Row({ item, style, onSelect }) { /* ... */ });

function List({ items }) {
  const [query, setQuery] = useState('');
  return items.map((item) => (
    <Row
      key={item.id}
      item={item}                      // stable if items is stable
      style={{ paddingLeft: 8 }}       // new object every render: memo always fails
      onSelect={() => choose(item.id)} // new function every render: same problem
    />
  ));
}

Hoisting the style object out of the component and lifting the callback to take the id as an argument fixes both without a single hook. Reach for useCallback when the handler genuinely closes over changing state, not as the default response.

Children is a prop, and it is never equal

The case people miss is composition. <Panel><Chart /></Panel> compiles to Panel receiving a children prop holding a freshly created element object. Element objects are recreated on every parent render, so a memoised Panel fails its comparison unconditionally. The way out is to move the memo boundary to where the expensive work lives — memoise Chart, not Panel — or to hoist the element into a variable in a component that does not re-render.

Two things memo cannot see

A memoised component still re-renders when its own state or a reducer it uses changes, which is correct and desirable: props did not change, but the component's own data did. It also re-renders when a context it consumes publishes a new value, because context propagation deliberately bypasses the props comparison. A component reading a context whose value is an object literal built in the provider's render will therefore re-render on every provider render, and no amount of memo on the way down will stop it. Memoising the provider's value, or splitting one wide context into narrower ones, is the fix there.

useMemo and useCallback are also weaker guarantees than they look. The documentation is explicit that they are performance hints, not semantic ones: React may discard a cached value, for instance to free memory. Never write code whose correctness depends on the cache surviving.

Re-render is not repaint

The reason so much memoisation delivers nothing measurable is that a re-render is a call to your function plus a diff of the returned tree. If the output is identical, React mutates no DOM. That work is usually cheap, and cheaper than the comparisons and cache bookkeeping you added to avoid it. What is expensive is a component that does real work per render — parsing, sorting a large array, formatting thousands of dates — or a tree so large that even trivially cheap renders add up.

So the order of operations matters. Record a profile first, find where the time is actually going, and only then decide whether the answer is memoisation, moving state down so fewer components subscribe to it, or windowing the list so 30 rows exist instead of 5,000. Virtualisation beats memoisation for long lists every time, because it removes the components rather than making them cheap.

One more trap sits underneath all of this: an unstable key. If keys change between renders, React unmounts and remounts the subtree, which destroys state and runs effects again. That is strictly worse than a re-render, and memo will not save you from it because the component is not being re-rendered at all — it is being replaced.

What the compiler changes

React Compiler, which reached a 1.0 release in 2025, applies memoisation automatically at build time by analysing which values a component actually depends on. Where it is enabled, manual useMemo, useCallback and memo calls become largely redundant and existing ones are safe to remove gradually. It does not change the underlying model, though: unstable keys still remount, context still propagates past comparisons, and a list of 5,000 rows is still a list of 5,000 rows.

Likely follow-ups

  • Your memoised row takes an onSelect prop from a parent that re-renders often. What are the two ways to make that prop stable, and which is preferable?
  • Why does passing children to a memoised component defeat memo, and what would you do instead?
  • A list of 5,000 rows is slow even with every row memoised. What is the actual fix?
  • What does the React Compiler remove the need for, and what does it still not solve?

Related questions

Further reading

reactmemoisationre-rendersreferential-equalityreact-compiler