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.
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
- How do you know a problem is dynamic programming?mediumAlso on memoisation5 min
- Angular, Vue and React are answers to the same problems. Compare how each handles change detection, reactivity and dependency injection.hardAlso on react5 min
- Why does my useEffect run twice on mount?easyAlso on react5 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: concept7 min
- How do you tell whether a value escapes to the heap, and how would you find the allocations that are costing you?hardSame kind of round: concept6 min
- How do you decide between BFS and DFS - and how do you recognise a graph problem that nobody described as a graph?mediumSame kind of round: coding4 min
- How do goroutines leak, and how would you find a leak in a running service?mediumSame kind of round: concept5 min
- Your function returns nil on the success path, the caller checks err != nil, and the check fires anyway. What happened?hardSame kind of round: concept4 min