What Does React.memo Do?
React.memo is a higher-order component that wraps a function component and prevents it from re-rendering unless its props actually change. By default React re-renders a child whenever the parent re-renders — regardless of whether the child's props changed. React.memo adds a prop-equality check before calling the component function again.
const Avatar = React.memo(function Avatar({ name, src }) {
console.log('Avatar rendered');
return <img src={src} alt={name} />;
});
// Avatar only re-renders when `name` or `src` changes,
// not every time the parent re-renders.
If the props are equal, React reuses the last rendered output. If they differ, it re-renders normally.
Shallow Equality — What "Equal" Means
React.memo uses shallow equality by default: it compares each prop with Object.is. This works perfectly for primitives (strings, numbers, booleans) but trips up on non-primitive props like objects, arrays, and functions, because those are compared by reference, not by value.
// This breaks memo — a new object is created on every parent render
function Parent() {
return <Chart options={{ color: 'blue' }} />; // new ref each render
}
// Fix: lift the constant out of the render function
const OPTIONS = { color: 'blue' };
function Parent() {
return <Chart options={OPTIONS} />;
}
Understanding shallow equality is the most important concept for React.memo interview questions — examiners often ask "why isn't memo working?" and the answer almost always comes down to unstable references.
When Is React.memo Worth Using?
Three conditions should all be true before reaching for React.memo:
- The component re-renders frequently — ideally confirmed with the React DevTools Profiler, not assumed.
- The component is expensive to render — complex DOM output, heavy calculations inside the component body, or deep subtrees.
- The props are stable — primitives or memoized references that don't change on every parent render.
Apply it at the boundary where data flows down into a subtree that doesn't need to update with every parent change — for example, a sidebar that only cares about the current user, not the rest of the page state.
The Inline-Function Pitfall
The most common React.memo bug: passing an inline callback as a prop. A new function reference is created on every parent render, making React.memo's equality check always fail.
// Broken — onClick is a new reference every render
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
{/* memo does nothing — handleClick is recreated every render */}
<ExpensiveList onItemClick={(id) => console.log(id)} />
</>
);
}
The fix is useCallback, which memoizes the function reference across renders:
function Parent() {
const [count, setCount] = useState(0);
// Stable reference — only changes if deps change
const handleItemClick = useCallback((id) => {
console.log(id);
}, []); // empty deps → created once
return (
<>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<ExpensiveList onItemClick={handleItemClick} />
</>
);
}
React.memo and useCallback are a pair — one rarely makes sense without the other when callbacks are involved.
Custom Comparators
You can override the default shallow-equality check by passing a second argument: a function that receives the previous and next props and returns true if the component should skip re-rendering.
const Chart = React.memo(
function Chart({ data, label }) {
return <canvas>{/* expensive render */}</canvas>;
},
(prevProps, nextProps) => {
// Only re-render when the data array length changes
// (ignoring internal value changes — intentional trade-off)
return prevProps.data.length === nextProps.data.length
&& prevProps.label === nextProps.label;
}
);
Use custom comparators sparingly. They are easy to get wrong (returning true when you should return false silently skips valid updates), and they add maintenance overhead. Prefer stable prop references over custom logic.
Context Subscriptions Bypass React.memo
This is a frequent interview gotcha: React.memo does not block re-renders triggered by context. If a memoized component calls useContext, it will re-render whenever that context value changes — regardless of whether the component's props changed.
const ThemeContext = createContext('light');
// This WILL re-render when ThemeContext changes,
// even though it's wrapped in React.memo
const Button = React.memo(function Button({ label }) {
const theme = useContext(ThemeContext);
return <button className={theme}>{label}</button>;
});
The fix is to split contexts so components only subscribe to the slice they need, or to use context selectors (a pattern enabled by libraries like use-context-selector).
The Children Prop Gotcha
Passing JSX as children creates a new React element object on every parent render, which defeats memo just like an inline object prop would.
// memo does nothing — children is a new element object each render
<MemoizedWrapper>
<SomeContent />
</MemoizedWrapper>
If you need to memoize a component that accepts children, either memoize the children separately or restructure so the wrapper accepts a render prop (a stable function reference via useCallback).
Class Component Equivalent: PureComponent
React.memo is the function-component equivalent of React.PureComponent. Both do a shallow prop comparison to skip unnecessary renders. PureComponent also does a shallow state comparison, which has no equivalent in React.memo (state comparison in function components is handled inside useState itself — React bails out of a re-render automatically when the new state is Object.is-equal to the old one).
When NOT to Use React.memo
- Cheap components — the equality check itself has a cost. For a component that renders a single
<span>, memo adds overhead without benefit. - Components that almost always receive new props — memo just wastes the comparison.
- Components near the root — they rarely re-render in the first place; the win is negligible.
- As a first resort — always profile before memoizing. Premature memoization adds cognitive overhead and can hide architectural problems (state living too high in the tree).
Rule of thumb: reach for React.memo only after the DevTools Profiler shows a component re-rendering more than it should and you have confirmed its props can be made stable. Fix unstable references with useCallback (functions) and useMemo (objects/arrays), then wrap with React.memo as the final step.