What is React.memo and why is it needed
React.memo is a higher-order component (HOC) that wraps a functional component and skips its re-render when props have not changed.
Theory
TL;DR
- React re-renders every child when a parent re-renders, even if the child's props are identical. React.memo adds a props check before that happens.
- Comparison is shallow: primitives compare by value, objects and functions compare by reference.
- Wrap in memo when React Profiler shows a component re-rendering with unchanged props on every parent update.
- Always pair with
useCallbackoruseMemowhen passing functions or objects as props, or the comparison will fail every time. - The second argument lets you define custom comparison logic for complex cases.
Quick example
Without memo, Button re-renders on every parent state change. The inline onClick is a new function reference each time, so even wrapping in memo without useCallback does nothing:
import { memo, useState, useCallback } from 'react';
const Button = memo(({ onClick }) => {
console.log('Button rendered');
return <button onClick={onClick}>Click</button>;
});
function App() {
const [count, setCount] = useState(0);
// Same reference between renders
const handleClick = useCallback(() => console.log('clicked'), []);
return (
<>
<Button onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>+1</button>
</>
);
}Button renders once. Clicking +1 triggers App to re-render, but handleClick keeps its reference. Memo's comparison returns equal, and Button stays untouched.
Key difference
React's default reconciliation re-renders every child whenever a parent renders, regardless of whether the child's output would change. React.memo inserts a shallow Object.is check on each prop before the render call. If all props match, React reuses the cached render output and skips the function call and DOM diff entirely. In trees with many leaf nodes that rarely receive new data, this reduces CPU work on every parent update.
When to use
- List items that stay the same most of the time: wrap each
<ListItem />in memo so unchanged items skip re-render when one item updates. - Components with slow render logic: charts, large filtered tables, nested lists.
- Pure display components with no local state.
- React Profiler shows a component consuming noticeable frame time with identical props across renders.
Skip memo when:
- The component is tiny and re-renders in under 0.1ms anyway.
- Props change on every render (an inline function without
useCallbackalways produces a new reference). - No profiler evidence of a problem. Memo adds comparison overhead even when it saves nothing.
How React.memo works internally
React stores the previous props and the last ReactElement output in the component's fiber node. On the next render, it runs shallowEqual: loops Object.keys, calls Object.is on each value pair. If every key matches, the cached element is returned and your component function never runs. The comparison costs roughly 1-2 microseconds. That only pays off if the saved render is meaningfully more expensive.
In React 18 with concurrent mode, memo still applies during startTransition renders. Urgent updates bypass the queue and memo runs fresh on those.
Custom comparison
When shallow comparison is not enough, pass a second argument:
const UserCard = memo(({ user }) => {
return <div>{user.name}</div>;
}, (prevProps, nextProps) => {
// true = skip re-render, false = allow re-render
return prevProps.user.id === nextProps.user.id;
});Return true means props are considered equal. Be careful with deep equality functions here: a slow comparison can cost more than the render it is trying to prevent.
Common mistakes
1. Inline functions as props
// Wrong: new function reference every render
<MemoChild onClick={() => alert(1)} />
// Fix
const handleAlert = useCallback(() => alert(1), []);
<MemoChild onClick={handleAlert} />Object.is(() => {}, () => {}) is always false. The comparison fails every time and memo does nothing.
2. Object literals created during render
// Wrong: new reference on every render
const user = { name: 'Alice', details: { age: 30 } };
return <UserProfile user={user} />;
// Fix
const user = useMemo(() => ({ name: 'Alice', details: { age: 30 } }), []);Shallow comparison checks the object reference, not its contents. A new literal is a new reference.
3. Mutating props objects in place
If you pass { items: [] } and mutate the array without replacing the reference, memo sees the same reference and skips the render. The UI shows stale data. Use immutable updates.
4. Memoizing tiny components without profiling first
A component that renders in 0.05ms costs more in comparison overhead than it saves. The React DevTools flamegraph shows exactly where time is being spent. Check there before adding memo anywhere.
5. Unstable children prop
// Every render creates a new React element
<MemoChild>
<span>label</span>
</MemoChild>children is compared by reference like any other prop. A new JSX expression is a new object. Stabilize with useMemo or extract static children to a module-level constant.
In practice, the most common issue is forgetting useCallback on event handlers, which quietly defeats memo on every component that receives them.
Real-world usage
- React Window: memoizes row components for lists with 10,000+ items.
- Material-UI:
TableRowis wrapped in memo to avoid re-renders during grid scroll. - TanStack Table: cell renderers are memoized to skip work during column sort and filter operations.
- TanStack Query: generated hooks memoize selector output the same way
reselectdoes for Redux. - Next.js: memo marks client-component boundaries inside server component trees to limit re-render scope.
Follow-up questions
Q: How does shallow comparison work when a prop is an object?
A: React loops Object.keys and calls Object.is on each value. Primitives compare by value. Objects, arrays, and functions compare by reference. Two different object literals with identical contents are not equal to Object.is.
Q: When does React.memo hurt performance?
A: When the component renders faster than the comparison takes. A 0.05ms render versus 0.1-0.3ms for the props check saves nothing. Profile with React DevTools before adding memo.
Q: What is the difference between React.memo and useMemo?
A: React.memo wraps a component and caches its render output based on props equality. useMemo is a hook that caches any computed value inside a component. You often use both together: memo on the component boundary, useMemo to stabilize an object you pass down as a prop.
Q: Can you control comparison manually?
A: Yes. The second argument is (prevProps, nextProps) => boolean. Return true to skip, false to allow. Deep equality functions work here but add their own cost, so benchmark before using them.
Q: How does React.memo behave with concurrent features in React 18?
A: Memo works with startTransition. During low-priority transitions, React may interrupt and restart renders, and memo's comparison runs on each attempt. Urgent updates (direct user input) are not deferred, so memo runs fresh there too. The fiber node tracks the component type via the MemoComponent tag.
Examples
Basic: memoized status badge
import { memo, useState } from 'react';
const StatusBadge = memo(({ status }) => {
console.log('StatusBadge rendered');
return <span className={`badge-${status}`}>{status}</span>;
});
function Dashboard() {
const [count, setCount] = useState(0);
return (
<div>
<StatusBadge status="active" />
<button onClick={() => setCount(c => c + 1)}>Clicks: {count}</button>
</div>
);
}StatusBadge renders once. Every subsequent click updates count and re-renders Dashboard, but status is still "active". String comparison passes, badge skips.
Intermediate: memoized todo list item
import { memo, useState, useCallback } from 'react';
const TodoItem = memo(({ id, text, completed, onToggle }) => {
console.log(`TodoItem ${id} rendered`);
return (
<label>
<input
type="checkbox"
checked={completed}
onChange={() => onToggle(id)}
/>
{text}
</label>
);
});
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Buy groceries', completed: false },
{ id: 2, text: 'Write tests', completed: false },
]);
const handleToggle = useCallback((id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
return todos.map(todo => (
<TodoItem key={todo.id} {...todo} onToggle={handleToggle} />
));
}Toggling item 1 creates a new object only for item 1. Item 2's props (id, text, completed, onToggle) are bit-for-bit the same, so its render is skipped. In a list of 200 items, one update skips 199 renders.
Advanced: when memo fails on nested objects
import { memo, useState, useMemo } from 'react';
const UserProfile = memo(({ user }) => {
console.log('UserProfile rendered');
return <div>{user.name} - {user.details.age}</div>;
});
function App() {
const [count, setCount] = useState(0);
// Bug: new object on every render
const user = { name: 'Alice', details: { age: 30 } };
return (
<>
<UserProfile user={user} />
<button onClick={() => setCount(c => c + 1)}>+1</button>
</>
);
}Every click creates a fresh user literal. Shallow comparison sees a new reference and re-renders every time. Memo does nothing.
Fix: stabilize the object.
const user = useMemo(() => ({ name: 'Alice', details: { age: 30 } }), []);Now user keeps its reference across renders unless the dependencies change, and memo works as expected.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.