React.memo accepts an optional second argument: a function that receives the previous and next props. What must that function return for React to skip the re-render, and what bugs do hand-written comparators commonly introduce?
answer
- true means equal, so skip
- inverted versus shouldComponentUpdate
- every omitted prop is a promise
- comparison can outweigh the render
- props only, not state or context
basics
~20 sReturn true when the props should be treated as equal, which tells React to skip the re-render — the opposite of the old shouldComponentUpdate convention. Hand-written comparators go wrong by ignoring props that matter, which freezes stale data on screen, or by deep-comparing large objects for more cost than the render saved.
solid answer
~50 sThe comparator has the shape `(prevProps, nextProps) => boolean`, and returning `true` means "these are equal, skip the render". That inversion catches people who remember `shouldComponentUpdate`, where returning `true` meant "do update". Two failure modes dominate. First, **incomplete comparison**: someone compares `prev.id === next.id` and quietly stops honouring every other prop, so a changed status or a replaced callback never reaches the DOM and the UI shows stale data — a correctness bug wearing a performance costume. Second, **comparison that costs more than the render**: deep-walking a large object on every parent render can easily exceed the price of just re-rendering some elements. React's own guidance is that most components need no custom comparator; if the default shallow check keeps failing, the real fix is usually to stabilise the offending prop rather than to write a comparator that ignores it.
code
javascript · 12 linesimport { memo } from 'react';
function RowImpl({ id, label, onSelect }) {
return <button onClick={() => onSelect(id)}>{label}</button>;
}
// Dangerous: onSelect is excluded, so the row can keep calling a handler
// captured from an earlier render.
export const Row = memo(
RowImpl,
(prev, next) => prev.id === next.id && prev.label === next.label
);go deeper
Know that React.memo takes an optional comparison function and that returning true means the props are considered equal, so the re-render is skipped.
Explain that the comparator replaces the default shallow check entirely, and that its polarity is inverted relative to the class-era shouldComponentUpdate.
Show the production judgment: an omitted prop becomes stale data or a stale handler, a deep comparison can cost more than the render, and stabilising the prop is usually the better fix.
Own the guidance that a custom comparator is a correctness surface a team must maintain as props evolve, and set the bar for when one is allowed at all rather than leaving it to individual taste.
## The signature and the inversion `React.memo` takes an optional second argument: ```jsx const Row = memo(RowImpl, (prevProps, nextProps) => { return prevProps.id === nextProps.id && prevProps.label === nextProps.label; }); ``` It is called with the previous props and the next props, and its boolean answers the question **"are these equal?"** — `true` means equal, therefore skip the render. This is deliberately the opposite polarity of the class-era `shouldComponentUpdate(nextProps, nextState)`, which answered "should I update?" so `true` meant re-render. Interviewers ask precisely because inverting it silently produces a component that either never updates or never bails out, with no error to point at. Supplying the comparator replaces the default shallow check entirely; React no longer performs its own per-prop `Object.is` pass. Everything the default would have caught is now your responsibility. ## Failure mode one: a comparator that lies The usual motivation for writing one is frustration: "this component keeps re-rendering because the parent passes a new `onSelect` arrow every time". So the comparator quietly omits `onSelect` from the comparison. Now the component bails out while holding a function from an earlier render — one that closes over older state. Click the row and it acts on data that has moved on. The symptom shows up far from the cause: an action applied to the wrong record, a form that submits a value the user already changed. The same shape appears with data props. Compare only `id` and the row keeps its old `status` forever, because nothing ever tells React that anything changed. This is the reason to treat a custom comparator as a correctness surface, not a tuning knob: every prop you leave out is a promise that the prop will never matter again, and nobody re-reads that promise when a new prop is added next quarter. ## Failure mode two: a comparison heavier than the render The other common version reaches for a deep-equality helper so that structurally identical objects compare equal. That can work, but the arithmetic must hold: the walk runs on **every** parent render, while the render it might skip runs only when the walk fails. Deep-comparing a large nested object or a thousand-element array to avoid re-rendering a component that emits a dozen DOM nodes is a straight loss. If you cannot state roughly how expensive the render is and roughly how expensive the comparison is, you are not in a position to claim the trade is positive. ## What the comparator does not see It receives props only. It has no view of the component's state, and no view of context values the component reads — a consumer still re-renders when its context updates, no matter what the comparator says. Candidates who think a comparator can suppress a context-driven render have the mental model wrong. And as with all React memoization, a bailout is an optimization React may decline to make. Nothing about the component's correctness may depend on the render being skipped. ## The better default When the default shallow comparison keeps failing, ask *why* the props are unstable rather than papering over it. Usually one of a small set of fixes applies: keep the value's identity stable across renders, hoist a genuinely constant value out of the component so it is allocated once, or change what crosses the boundary so the volatile value never becomes a prop of the memoized component at all. Those fixes make the default comparison work honestly. A comparator that excludes props to force a bailout only converts a render cost into a stale-UI risk. A reasonable rule to state in an interview: reach for a custom comparator only when a prop is genuinely structural rather than referential — say a value object rebuilt each render from the same fields — the render being skipped is measurably expensive, and every prop is accounted for in the comparison. Otherwise use the default, or none at all.
- A comparator returns true for every pair of props. What does the component do?It renders once and then never re-renders from a props change again, because it always reports its props as equal. State and context updates still reach it, so the result is a confusing half-frozen component: some updates land, prop-driven ones never do. It is a correctness bug that no warning will surface.
- Why is a deep-equality comparator often the wrong answer to unstable object props?Because it runs on every parent render, while the render it avoids only happens when it fails. Deep-walking a large structure to skip a cheap render loses outright. It also hides the actual problem — a value being rebuilt each render — which is usually fixable at the source with far less machinery.
- Can a custom comparator stop a re-render caused by the component's own state update?No. The comparator sits on the props path and is consulted only when a parent render would push new props down. A `useState` or `useReducer` update inside the component re-renders it regardless, and so does a change to any context value the component reads.
saying these in an interview costs you the question
- Returns true meaning 'should update', copying shouldComponentUpdate
- Excludes function props from the comparison to force a bailout
- Reaches for deep equality without weighing its cost
- Thinks the comparator can suppress state or context updates
- Believes supplying a comparator keeps React's default shallow check too