React.memo is a higher-order component that wraps a function component
and skips re-rendering it when its props are shallowly equal to the previous
render's props.
const Button = React.memo(function Button({ label, onClick }) {
console.log('render')
return <button onClick={onClick}>{label}</button>
})
// Parent re-renders with same label and onClick reference:
// → Button's console.log does NOT fire
Under the hood, before calling your component function React compares each
old prop to each new prop with Object.is. If all are equal, it reuses the
previous render output.
React.memo only applies to function components. The class-component
equivalent is PureComponent (or shouldComponentUpdate).
Rule of thumb: React.memo is a performance hint, not a correctness
guarantee — React may still re-render the component in some cases (e.g.,
concurrent rendering). Never rely on it to prevent side effects from firing.
Shallow equality compares values one level deep using Object.is:
primitives are compared by value; objects and functions are compared by
reference.
Object.is(1, 1) // true — same primitive
Object.is('a', 'a') // true — same primitive
Object.is([], []) // false — different object references
Object.is({ x: 1 }, { x: 1 }) // false — different object references
This is why inline objects and functions as props break memoisation:
// BAD: new object on every render → memo always re-renders
<Chart options={{ color: 'red' }} />
// GOOD: stable reference, memo works
const OPTIONS = { color: 'red' }
<Chart options={OPTIONS} />
// GOOD: memoised object
const options = useMemo(() => ({ color }), [color])
<Chart options={options} />
Rule of thumb: Pass primitives when possible; stabilise object and
function props with useMemo / useCallback when using React.memo.
React.memo pays off when all three conditions hold:
- The component renders frequently (its parent re-renders often).
- The component renders expensively (large subtree, heavy computation).
- Its props are stable (primitives or memoised references).
// Worth memoising: renders on every keystroke, large list
const ResultList = React.memo(function ResultList({ items }) {
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)
})
// Not worth memoising: renders rarely, trivially cheap
const PageTitle = React.memo(function PageTitle({ text }) {
return <h1>{text}</h1>
})
The overhead of React.memo itself (the shallow comparison on every parent
render) can exceed the savings if the component is cheap or its props are
unstable objects.
Rule of thumb: Profile first. Reach for React.memo only when the
Profiler shows the component actually re-renders unnecessarily and the
re-render is measurably expensive.
Pass a second argument to React.memo: a function that receives the
previous and next props and returns true if the component should
skip re-rendering (i.e., they are "equal enough").
const Chart = React.memo(
function Chart({ data, width }) {
return <canvas ref={drawChart(data, width)} />
},
(prevProps, nextProps) => {
// Skip re-render if only the data length changed, not actual values
return (
prevProps.width === nextProps.width &&
prevProps.data.length === nextProps.data.length &&
prevProps.data.every((v, i) => v === nextProps.data[i])
)
}
)
Important: returning true means do NOT re-render (the opposite of
shouldComponentUpdate, which returns true to allow a re-render). This
is a common source of confusion.
Rule of thumb: Custom comparators add complexity and can silently suppress needed updates if written incorrectly. Use them only when the default shallow comparison is measurably too eager.
Each render creates a new function object, so the reference changes
every time, causing React.memo's shallow comparison to always return
"not equal" and re-render anyway.
function Parent() {
const [count, setCount] = useState(0)
// New function reference on every Parent render → memo useless
return <Child onClick={() => console.log('click')} />
}
const Child = React.memo(function Child({ onClick }) {
console.log('Child render') // still fires every time
return <button onClick={onClick}>Click</button>
})
Fix with useCallback to stabilise the reference:
function Parent() {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => {
console.log('click')
}, []) // stable reference — same function object across renders
return <Child onClick={handleClick} />
}
Rule of thumb: React.memo and useCallback are best friends — you
almost always need both: memo to skip the child render, and useCallback to
prevent the function prop from busting the memo.
Both perform a shallow prop comparison to avoid unnecessary re-renders, but they apply to different component types.
React.memo |
PureComponent |
|
|---|---|---|
| Works with | Function components | Class components |
| Custom equality | 2nd argument comparator | Override shouldComponentUpdate |
| Checks state | No (state is inside the hook, not a prop) | Yes — shallow-compares both props and state |
// Function component
const Foo = React.memo(function Foo(props) { … })
// Class component
class Foo extends React.PureComponent {
render() { … }
}
PureComponent also shallow-compares this.state, which React.memo
has no equivalent for (state is internal to hooks and React itself handles
bailing out when the state reference doesn't change).
Rule of thumb: In modern React (function components), React.memo is the
standard tool. PureComponent is for legacy class components.
No. React.memo only skips re-renders caused by changed props.
If a memoised component subscribes to a context (via useContext) and
the context value changes, the component will re-render regardless
of whether its props changed.
const ThemeContext = createContext('light')
const Card = React.memo(function Card({ title }) {
const theme = useContext(ThemeContext) // subscribed to context
return <div className={theme}>{title}</div>
})
// Parent provides a new context value:
// Card re-renders even though `title` prop didn't change
Mitigation strategies:
- Split context into multiple smaller contexts so unrelated consumers don't re-render.
- Use
useMemoinside the component to memoize the expensive derived output, even if render is called. - Use a state-management library (Zustand, Jotai) that uses selectors to subscribe only to the slice of state a component needs.
Rule of thumb: React.memo and context subscriptions are orthogonal —
to limit context-driven re-renders you need to narrow the context or use
selectors, not memo.
When you write <Wrapper><Child /></Wrapper>, React creates a new element
object for <Child /> on every render of the parent where the JSX
lives. Because element objects are new references each time, React.memo's
shallow comparison sees a changed children prop and re-renders.
function Parent() {
const [count, setCount] = useState(0)
// New <div>Hello</div> element object created every render
return <Wrapper>{<div>Hello</div>}</Wrapper>
}
const Wrapper = React.memo(function Wrapper({ children }) {
console.log('Wrapper render') // fires on every Parent re-render!
return <section>{children}</section>
})
Fixes:
- Move the children JSX outside the re-rendering parent (lift it up).
- Wrap children in
useMemoif they depend on parent state. - Restructure so
Wrapperdoesn't need to be memoised.
Rule of thumb: Memoising components that accept children is often
counter-productive — consider memoising the children themselves instead, or
rethinking the component hierarchy.
Avoid React.memo when:
- Props change on every render anyway — memo runs the comparison but still re-renders, adding overhead for zero gain.
- The component is trivially cheap — shallow-comparing many props can cost more than just running the component.
- Props include non-stabilised objects or functions — memo is immediately defeated and the comparison is wasted work.
- You haven't profiled — premature memoisation clutters the code and makes it harder to refactor.
// No point memoising — title is a new string computed in parent every render
const Title = React.memo(({ title }) => <h1>{title}</h1>)
function Parent({ user }) {
return <Title title={`Hello, ${user.name}!`} /> // new string each render
}
Rule of thumb: Reach for React.memo only after the Profiler confirms
a component re-renders unnecessarily AND that the re-render is expensive.
Default to no memoisation.
When you wrap an anonymous function, React DevTools shows the component as
Memo or Anonymous, making the component tree hard to read. Two ways to
preserve the name:
// Option 1: named function expression (recommended)
const Button = React.memo(function Button({ label }) {
return <button>{label}</button>
})
// DevTools shows: Memo(Button)
// Option 2: set displayName explicitly
const Button = React.memo(({ label }) => <button>{label}</button>)
Button.displayName = 'Button'
// Option 3: extract and export the inner component
function ButtonBase({ label }) { return <button>{label}</button> }
export const Button = React.memo(ButtonBase)
Rule of thumb: Always use a named function expression as the first
argument to React.memo — it's the simplest and least error-prone way to
keep DevTools readable.
Wrap with both — the order is React.memo(React.forwardRef(...)).
forwardRef should be the inner wrapper because it transforms the
(props, ref) signature; memo wraps the result.
import { forwardRef, memo } from 'react'
const FancyInput = memo(
forwardRef(function FancyInput({ label }, ref) {
return (
<label>
{label}
<input ref={ref} />
</label>
)
})
)
// Usage:
const inputRef = useRef()
<FancyInput ref={inputRef} label="Name" />
inputRef.current.focus()
DevTools will show: Memo(ForwardRef(FancyInput)).
Rule of thumb: Compose in the order memo(forwardRef(fn)) — memo on
the outside handles the prop comparison; forwardRef on the inside handles
the ref plumbing.
shouldComponentUpdate(nextProps, nextState) is the class-component
lifecycle that lets you control whether a component re-renders. Returning
false skips the render; returning true allows it.
React.memo is the functional-component analogue that handles the same
optimisation for props only (it has no access to internal state, which React
manages automatically).
// Class component
class List extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.items !== this.props.items // custom check
}
render() { … }
}
// Equivalent with function component + memo
const List = React.memo(
function List({ items }) { … },
(prev, next) => prev.items === next.items // same semantics, inverted return
)
Key difference: shouldComponentUpdate returning false skips the update;
memo's comparator returning true skips the update (inverted meaning).
Rule of thumb: In new code, prefer function components with React.memo.
Use shouldComponentUpdate only when maintaining or extending legacy class
components.
When a list contains many items and the parent state changes frequently (e.g., a filter input), each item component gets re-rendered on every keystroke even if its own data hasn't changed. Memoising the item component keeps only the changed item rendering.
// Without memo: all 1000 items re-render on every keystroke
function ItemRow({ item }) {
return <tr><td>{item.name}</td><td>{item.price}</td></tr>
}
// With memo: only items whose props changed re-render
const ItemRow = React.memo(function ItemRow({ item }) {
return <tr><td>{item.name}</td><td>{item.price}</td></tr>
})
// Parent
function Table({ rows }) {
return (
<tbody>
{rows.map(row => <ItemRow key={row.id} item={row} />)}
</tbody>
)
}
For this to work, rows elements must be stable references — if the parent
rebuilds the array on every render, each item prop is a new object and
memo is defeated. Stabilise with useMemo.
Rule of thumb: For large lists (50+ items) where individual items are
pure, React.memo on the row component is one of the highest-ROI
optimisations you can make.
On every parent render, React must:
- Look up the previous props object for the memoised child.
- Run
Object.ison every prop pair (or your custom comparator). - Either skip the child render (if equal) or proceed (if not).
For a component with many props or complex prop objects, step 2 can be non-trivial — especially if the comparator does deep equality or array iteration.
// 20 props, comparator runs 20 Object.is calls on every parent render
const HeavyCard = React.memo(function HeavyCard({
id, title, description, imageUrl, price, rating,
inStock, category, tags, author, createdAt, updatedAt,
onClick, onHover, onFocus, isSelected, isFeatured,
discount, currency, locale
}) { … })
If the child renders in <1 ms and the parent re-renders infrequently, the memo overhead (prop comparison) may cost more than the render it prevents.
Rule of thumb: Memoize components with few, stable-typed props. For components with many props, the comparison cost can rival the render cost — measure before committing.
More Rendering and Performance interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.