What Is React.memo and How Does It Help with Performance?
Advertisement
🧩 Scenario
Architecture Walkthrough
How React.memo Intercepts Re-renders
Normally, when a parent component re-renders, React re-renders every child regardless of whether their props changed. This is fine for cheap components — React's rendering is fast — but it becomes a bottleneck when a child does expensive work or when a parent re-renders at high frequency (every keystroke or scroll event). React.memo wraps a component in a higher-order component that compares the previous and next props before deciding to re-render. If the comparison returns equal, React reuses the last rendered output without calling the component function.
The comparison uses Object.is on each prop individually. For primitives (strings, numbers, booleans), this behaves like === and works correctly. For objects and arrays, Object.is compares references, not contents. An inline object { color: 'red' } is a new object on every parent render, so the reference differs each time and the comparison reports inequality.
The Reference Trap
Functions are the most common trap. An arrow function defined in the parent's render body is a new function instance on every render. Passing onAddToCart={() => dispatch(addItem(id))} directly to a React.memo-wrapped child means the child re-renders on every parent render anyway, because the function reference is new. The memo comparison runs (overhead), finds the function prop unequal (new reference), and re-renders (no savings). This is worse than no memo at all.
The fix is useCallback on the parent side to stabilize function references, and useMemo for object and array props. The two sides of the optimization are inseparable: React.memo on the child and stable references on the parent.
Context Does Not Respect React.memo
A component that reads from useContext re-renders whenever the context value changes. React.memo wraps props comparison, not context comparison. If the context value object is recreated on every parent render (a common mistake with inline value={{ user, setUser }}), every consumer re-renders on every render of the provider's parent, regardless of React.memo wrapping.
Key Code Explained
import { memo, useCallback, useMemo, useState } from 'react';
// React.memo wraps the component — shallow prop comparison before each render
const ProductCard = memo(function ProductCard({
product,
onAddToCart,
}: {
product: Product;
onAddToCart: (id: string) => void;
}) {
console.log(`Rendering: ${product.name}`); // logs only when actually re-rendering
return (
<div className="card">
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => onAddToCart(product.id)}>Add to cart</button>
</div>
);
});
// Parent: must stabilize references for memo to work
function ProductGrid({ products }: { products: Product[] }) {
const [cartCount, setCartCount] = useState(0);
// Without useCallback: new function reference on every render
// ProductCard sees onAddToCart as changed every time — memo is bypassed
// With useCallback: same reference across renders (unless deps change)
const handleAddToCart = useCallback((id: string) => {
setCartCount((prev) => prev + 1);
// addToCart(id)...
}, []); // no deps — this function never changes
return (
<div>
<p>Cart items: {cartCount}</p>
{products.map((product) => (
<ProductCard
key={product.id}
product={product} // from a stable array prop — reference stable
onAddToCart={handleAddToCart} // stable via useCallback
/>
))}
</div>
);
}
// Inline object trap — same problem as inline functions
const STABLE_STYLE = { color: 'red' }; // defined outside render — stable reference
function BadExample() {
return (
// Wrong: new object reference on every render of BadExample
<ProductCard style={{ color: 'red' }} onAddToCart={handleAddToCart} />
);
}
function GoodExample() {
const style = useMemo(() => ({ color: 'red' }), []); // stable reference
return <ProductCard style={style} onAddToCart={handleAddToCart} />;
// Or: just define STABLE_STYLE outside the component (preferred for truly static values)
}
// Custom arePropsEqual: second arg to React.memo
// Use when one prop is an object that changes reference but not content
const DeepMemoCard = memo(
function DeepMemoCard({ config, onAction }: Props) {
return <div>{config.title}</div>;
},
(prevProps, nextProps) => {
// Return true to SKIP re-render (props "equal"), false to re-render
// Danger: if this function is wrong, components don't re-render when they should
return (
prevProps.config.id === nextProps.config.id &&
prevProps.config.title === nextProps.config.title &&
prevProps.onAction === nextProps.onAction
);
}
);
The console.log inside ProductCard is the quickest debugging tool. If it logs on every parent render despite React.memo, a prop reference is unstable. Check each prop passed to the component, starting with functions and objects. Identifying which prop is causing the re-render takes seconds; fixing it with useCallback or useMemo takes one line.
Tradeoffs
| Approach | Prevents re-render when | Does not prevent re-render when | Best used for |
|---|---|---|---|
React.memo (defaults) | Primitive props unchanged | Objects/functions with new references; context changes | Expensive components with stable primitive props |
React.memo + useCallback/useMemo | All stable props unchanged | Context changes | Expensive components with object/function props |
Custom arePropsEqual | Your logic says equal | Your logic is wrong | Deep object comparison on specific keys |
| No memoization | Never | Always (re-renders with parent) | Cheap components |
What Interviewers Actually Check
- Whether you can explain
Object.isshallow comparison and predict which prop types will and will not benefit - Whether you identify the inline object and function reference trap without being prompted
- Whether you pair
React.memowithuseCallbackanduseMemoon the parent - Whether you know
React.memodoes not prevent context-triggered re-renders - Whether you describe the workflow: profile first, identify bottlenecks, apply memo surgically
Follow-Up Questions
- A
ProductCardinReact.memois still re-rendering. You confirmproductandonAddToCartare stable. What else could cause this? React.memohas a second argument. When would you use it, and what are the risks of an incorrect comparator?- You have a list of 500 memoized items. The parent re-renders on every keystroke. Describe the computation React performs and when this optimization pays off.
- A colleague says "just add
React.memoto every component as a default." What is the argument against this as a blanket rule? - Your design system ships a
<Card>component that is not wrapped inReact.memoand you cannot modify it. How do you prevent unnecessary re-renders of it inside yourProductGrid?
Common Candidate Mistakes
- Adding
React.memoto a component but passing inline arrow functions as props — adds comparison overhead with zero savings - Thinking
React.memoprevents context-triggered re-renders — it does not - Using
React.memowithout profiling — many components are cheap enough that the comparison cost exceeds the re-render cost - Writing a custom
arePropsEqualfunction that has a bug — causes stale renders that are extremely hard to debug - Applying
React.memowithout also stabilizing prop references withuseCallback/useMemoon the parent
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you explain what
React.memodoes and what comparison it uses? - Can you explain why inline object and function props defeat memoization?
- Can you pair
React.memowithuseCallbackanduseMemoto make the optimization work? - Can you explain that context changes bypass
React.memo? - Can you describe the profiling-first workflow and explain why you do not apply memo everywhere?
Summary
React.memo prevents a function component from re-rendering when its props are shallowly equal to the previous render. Shallow equality uses Object.is per prop, which works correctly for primitives but compares objects and functions by reference. Inline object literals and arrow functions create new references on every parent render, silently defeating memoization: the component still re-renders every time, and you pay the overhead of the comparison for nothing. Pair React.memo on the child with useCallback for function props and useMemo for object/array props on the parent. React.memo does not prevent re-renders triggered by useContext changes. The correct workflow is to profile with React DevTools first to identify which components are actually causing slowness, then apply React.memo only to those components. Applying it everywhere adds comparison overhead for components that re-render due to unstable props anyway.
Does React.memo prevent re-renders caused by context changes?
No. React.memo only compares props. A component that reads from useContext will re-render whenever the context value changes, regardless of React.memo wrapping. To prevent context-triggered re-renders, either split the context into smaller contexts or memoize the context value with useMemo.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement