React 46 ⚛️ Component Memoization with React memo
React re-renders a component when its state changes, when its props change, or when its parent re-renders. The third case is the one that surprises developers. A parent re-renders because of its own state change, and every child re-renders with it, even the children whose props have not changed. For a small component tree, this is not a problem: the re-render is fast, and the reconciliation is cheap. For a large tree, or for a child whose render is expensive, the unnecessary re-renders add up. React.memo is the higher-order component that wraps a component and tells React to skip the re-render when the props have not changed. It is the memoization for the components, and it is the counterpart to useMemo and useCallback.
The mechanism is a shallow comparison of the props. When the parent re-renders, React renders the child, and before committing the result, it compares the new props with the previous props. If every prop is Object.is equal to its previous value, React skips the render and reuses the previous output. If any prop differs by reference, the render runs. The comparison is shallow: it checks the top-level props, not their contents. An object prop whose contents changed but whose reference is the same passes the comparison, and the child does not re-render — which may be correct or may be the bug, depending on the case.
The shallow comparison is the source of the most common React.memo confusion. When the parent passes an object literal, an array literal, or a function expression, the reference is new on every render, and the comparison fails. The child re-renders, and the memoization is defeated. The fix is the useMemo for the object and the array, and the useCallback for the function. The React.memo and the hooks work together: the memo stops the re-render when the props are unchanged, and the hooks keep the props unchanged when the contents are unchanged.
This chapter covers three areas. First, why React.memo exists — the unnecessary-re-render problem and the cost of the re-render. Second, how React.memo works — the shallow comparison, the custom comparator, the display name, and the interaction with the other features. Third, the patterns and the pitfalls — the stable props with the useMemo and the useCallback, the children as props, the context interaction, and the cases where the memo hurts more than it helps. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the memo comparison.
Key point: React.memo is a higher-order component that memoizes a component. It skips the re-render when the props are shallowly equal to the previous props. The comparison is Object.is on each top-level prop. The memo is defeated when the parent passes the new object, the array, or the function references on every render; the fix is the useMemo and the useCallback. The React.memo is the optimization for the expensive renders, not the default.
Why React.memo exists
The unnecessary-re-render problem. When a parent re-renders, React re-renders every child by default, regardless of whether the child’s props changed. The reason is that React cannot know whether the child’s output depends on the parent’s state; the child may read the context, use the state, or call a function that depends on the parent. The safe default is to re-render and let the reconciliation determine whether the DOM must change. The default is correct, but it is wasteful when the child’s props are unchanged and the child’s render is expensive .
The expensive-render problem. Some components are expensive to render. A large list, a complex chart, a heavy data table, a deeply nested tree. The render runs the component function, creates the elements, and reconciles the output. For the expensive components, the render is the bottleneck, and the unnecessary re-renders are the waste. The React.memo skips the render when the props are unchanged, and the savings are proportional to the render cost.
The stable-props problem. The React.memo only helps when the props are stable. A parent that passes the new object on every render defeats the memo, because the reference changes and the comparison fails. The useMemo and the useCallback are the hooks that keep the references stable. The three work together: the parent uses the hooks to stabilize the props, and the child uses the memo to skip the render when the props are the same.
The children-as-props problem. The children prop is a special case. When a parent re-renders, the children element is created fresh, and its reference changes. A memoized component that receives the children re-renders, because the children prop is new. The pattern is to pass the children as a prop from a stable parent, or to use the useMemo to stabilize the children element. The children prop is the common source of the defeated memo.
The context problem. A component that reads the context re-renders when the context value changes, even if it is wrapped in the React.memo. The memo compares the props, not the context. The context is a separate channel, and the memo does not stop the context-driven re-renders. The fix is the context splitting or the selector-based subscription.
The trade-off. The React.memo has the cost: the comparison of the props, and the memory for the previous props and the output. For the cheap components, the comparison is more expensive than the render, and the memo is a net loss. For the expensive components, the memo is a net gain. The rule is to apply the memo when the profiler shows the expensive re-renders, not by default .
a. The basic React.memo
The React.memo wraps a component and returns a memoized version. The memoized component renders the same output for the same props.
import { memo } from 'react';
const ExpensiveList = memo(function ExpensiveList({ items }) {
console.log('ExpensiveList rendered');
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
export default ExpensiveList;
The memo wraps the function component and returns the memoized version. When the parent re-renders and the items prop is the same reference, the ExpensiveList does not re-render, and the “ExpensiveList rendered” is not logged.
The comparison is shallow. React compares each prop with Object.is. The primitive props are the value comparison, and the objects and the functions are the reference comparison.
<ExpensiveList items={items} /> // the same reference → the skip
<ExpensiveList items={[...items]} /> // the new reference → the render
<ExpensiveList items={items.slice()} /> // the new reference → the render
The [...items] and the items.slice() create the new arrays, and the memo is defeated.
The display name. The memoized component has the display name of the wrapped component, but the DevTools may show the Memo wrapper. The displayName property sets the name:
const ExpensiveList = memo(function ExpensiveList({ items }) { ... });
ExpensiveList.displayName = 'ExpensiveList';
The named function expression also sets the name, which is the cleaner pattern.
b. The custom comparator
The React.memo accepts the second argument: the custom comparator. The comparator receives the previous props and the next props, and returns true if the props are equal (the skip) and false if they differ (the render).
const ExpensiveList = memo(
function ExpensiveList({ items }) { ... },
(prevProps, nextProps) => {
return prevProps.items.length === nextProps.items.length;
}
);
The comparator above compares the items by the length. If the lengths are equal, the props are considered equal, and the component does not re-render. The comparator is the way to customize the comparison when the shallow comparison is not the right one.
The comparator must be the pure function: it must not have the side effects, and it must return the consistent result. The comparator that returns true incorrectly causes the stale UI, and the comparator that returns false always defeats the memo.
The comparator anti-pattern. The comparator that does the deep comparison is the common anti-pattern. The deep comparison is expensive, and it often costs more than the render it avoids. The recommendation is to compare the identity or the few key fields, not the whole object.
// ❌ The deep comparison in the comparator
(prevProps, nextProps) => JSON.stringify(prevProps) === JSON.stringify(nextProps)
// ✅ The comparison of the key fields
(prevProps, nextProps) => prevProps.id === nextProps.id
The JSON.stringify is the expensive and the order-dependent. The comparison of the key fields is the cheap and the reliable.
c. The patterns and the pitfalls
The stable props with the useMemo. The parent uses the useMemo to stabilize the object and the array props:
function Parent({ items }) {
const config = useMemo(() => ({ sortBy: 'name', limit: 10 }), []);
return <ExpensiveList items={items} config={config} />;
}
The config object is the same reference across the renders, and the memoized ExpensiveList skips the render when the items and the config are unchanged.
The stable props with the useCallback. The parent uses the useCallback to stabilize the function props:
function Parent({ items, onSelect }) {
const handleSelect = useCallback((id) => {
onSelect(id);
}, [onSelect]);
return <ExpensiveList items={items} onSelect={handleSelect} />;
}
The handleSelect function is the same reference across the renders (as long as the onSelect is stable), and the memoized ExpensiveList skips the render.
The children-as-props. The memoized component that receives the children re-renders when the parent re-renders, because the children element is the new reference. The fix is to pass the children as the prop from a stable parent, or to use the useMemo.
// ❌ The children prop defeats the memo
function Parent() {
return (
<MemoizedLayout>
<ExpensiveList items={items} />
</MemoizedLayout>
);
}
// ✅ The children as a prop
function Parent() {
const children = useMemo(() => <ExpensiveList items={items} />, [items]);
return <MemoizedLayout children={children} />;
}
The useMemo stabilizes the children element, and the MemoizedLayout skips the render when the children are unchanged.
The context interaction. A memoized component that reads the context re-renders when the context value changes, regardless of the memo. The memo compares the props, not the context. The fix is the context splitting or the selector-based subscription.
The functional-update pattern. The memoized component that receives the state setter and the state value. The setState is stable, and the state value changes. The memoized component re-renders when the state changes, which is the desired behavior. The React.memo does not stop the state-driven re-renders.
The profiler is the tool. The React DevTools Profiler shows which components re-render and how long they take. The memo should be applied when the profiler shows the expensive re-renders, not by default. The premature memoization adds the comparison cost without the benefit.
Complete Example Session
// ============================================
// PART 1: THE UNMEMOIZED COMPONENT
// ============================================
function ExpensiveList({ items }) {
console.log('ExpensiveList rendered');
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
function Parent() {
const [count, setCount] = useState(0);
const items = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExpensiveList items={items} />
</>
);
}
// Every click re-renders the Parent, and the ExpensiveList
// re-renders too, even though the items are the same.
// ============================================
// PART 2: THE MEMOIZED COMPONENT
// ============================================
const ExpensiveList = memo(function ExpensiveList({ items }) {
console.log('ExpensiveList rendered');
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
// Still re-renders, because the items array is created fresh
// in the Parent on every render.
// ============================================
// PART 3: THE STABLE ITEMS
// ============================================
const ITEMS = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExpensiveList items={ITEMS} />
</>
);
}
// The ITEMS is the module-level constant, so the reference
// is the same. The memoized ExpensiveList does not re-render.
// ============================================
// PART 4: THE useMemo FOR THE ITEMS
// ============================================
function Parent({ sourceItems }) {
const [count, setCount] = useState(0);
const items = useMemo(() => sourceItems.filter((i) => i.active), [sourceItems]);
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExpensiveList items={items} />
</>
);
}
// The items array is stable as long as the sourceItems is stable.
// ============================================
// PART 5: THE useCallback FOR THE CALLBACK
// ============================================
const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
function Parent({ items, onSelect }) {
const handleSelect = useCallback((id) => {
onSelect(id);
}, [onSelect]);
return <ExpensiveList items={items} onSelect={handleSelect} />;
}
// The handleSelect is stable as long as the onSelect is stable.
// ============================================
// PART 6: THE CUSTOM COMPARATOR
// ============================================
const ExpensiveList = memo(
function ExpensiveList({ items, title }) {
return (
<>
<h2>{title}</h2>
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</>
);
},
(prevProps, nextProps) => {
return prevProps.items.length === nextProps.items.length;
}
);
// The memo skips the render when the items length is unchanged,
// even if the title changed. The comparator is the responsibility.
// ============================================
// PART 7: THE CHILDREN AS PROPS
// ============================================
const MemoizedLayout = memo(function Layout({ children }) {
console.log('Layout rendered');
return <div className="layout">{children}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<MemoizedLayout>
<ExpensiveList items={ITEMS} />
</MemoizedLayout>
</>
);
}
// The Layout re-renders on every click, because the children
// element is created fresh on every render.
// ============================================
// PART 8: THE STABLE CHILDREN
// ============================================
function Parent() {
const [count, setCount] = useState(0);
const children = useMemo(() => <ExpensiveList items={ITEMS} />, []);
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<MemoizedLayout>{children}</MemoizedLayout>
</>
);
}
// The children element is stable, so the Layout does not re-render.
// ============================================
// PART 9: THE CONTEXT INTERACTION
// ============================================
const ThemeContext = createContext('light');
const MemoizedButton = memo(function Button({ label }) {
const theme = useContext(ThemeContext);
console.log('Button rendered');
return <button className={theme}>{label}</button>;
});
// The Button re-renders when the theme changes, even though
// the label is the same. The memo does not stop the context
// re-renders.
// ============================================
// PART 10: THE COMPLETE OPTIMIZED COMPONENT
// ============================================
const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
function Parent({ sourceItems, onSelect }) {
const [count, setCount] = useState(0);
// The stable items.
const items = useMemo(
() => sourceItems.filter((i) => i.active),
[sourceItems]
);
// The stable callback.
const handleSelect = useCallback(
(id) => {
onSelect(id);
},
[onSelect]
);
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExpensiveList items={items} onSelect={handleSelect} />
</>
);
}
// The ExpensiveList re-renders only when the items or the
// handleSelect changes. The count changes do not affect it.
The ten parts show the unmemoized component, the memoized component, the stable items, the useMemo for the items, the useCallback for the callback, the custom comparator, the children as props, the stable children, the context interaction, and the complete optimized component.
Quick Reference
The React.memo API
| Form | Meaning |
|---|---|
memo(Component) | The shallow comparison |
memo(Component, areEqual) | The custom comparator |
Component.displayName = 'Name' | The DevTools name |
| The return | The memoized component |
The Shallow Comparison
| Prop type | The comparison |
|---|---|
| The primitive | The value (Object.is) |
| The object | The reference |
| The array | The reference |
| The function | The reference |
| The children | The reference |
The Memo Defeaters
| Defeater | The fix |
|---|---|
| The object literal | The useMemo |
| The array literal | The useMemo |
| The function expression | The useCallback |
The children element | The useMemo or the prop |
| The context value | The context splitting |
The Custom Comparator
| The return | The behavior |
|---|---|
true | The props are equal; the skip |
false | The props differ; the render |
| The deep comparison | The anti-pattern |
| The key comparison | The recommended |
The When to Use
| The condition | The memo? |
|---|---|
| The expensive render | Yes |
| The frequent re-renders | Yes |
| The stable props | Yes |
| The cheap component | No |
| The props change often | No |
| The context consumer | The memo does not help |
Best Practices
✅ Do This:
// Wrap the expensive component in the memo
const ExpensiveList = memo(function ExpensiveList({ items }) { ... }); // ✅
// Use the useMemo for the object props
const config = useMemo(() => ({ sortBy: 'name' }), []); // ✅
// Use the useCallback for the function props
const handleSelect = useCallback((id) => { ... }, [onSelect]); // ✅
// Use the named function expression for the DevTools
const List = memo(function List({ items }) { ... }); // ✅
// Use the custom comparator for the specific comparison
memo(Component, (prev, next) => prev.id === next.id); // ✅
// Measure with the Profiler before the memo
// The React DevTools Profiler // ✅
❌ Don’t Do This:
// Don't memoize the cheap components
const Label = memo(function Label({ text }) { return <span>{text}</span>; }); // ❌
// Don't pass the new object on every render
<ExpensiveList items={[...items]} /> // the new reference // ❌
// Don't pass the new function on every render
<ExpensiveList onSelect={() => {}} /> // the new reference // ❌
// Don't use the deep comparison in the comparator
memo(Component, (a, b) => JSON.stringify(a) === JSON.stringify(b)); // ❌
// Don't expect the memo to stop the context re-renders
const Button = memo(function Button() { const theme = useContext(...); }); // ❌
// Don't use the memo by default
// Apply it when the profiler shows the problem // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The memo does not work | The new prop reference | Use the useMemo or the useCallback |
| The children defeat the memo | The new element reference | Use the useMemo |
| The context re-renders | The memo compares the props | The context splitting |
| The stale UI | The comparator returns true incorrectly | Check the comparator |
| The memo is slower | The cheap component | Remove the memo |
| The custom comparator is expensive | The deep comparison | Compare the key fields |
| The props change often | The unstable parent | The stable props |
Real-World Examples
1. The Basic Memo
const ExpensiveList = memo(function ExpensiveList({ items }) { ... });
2. The Stable Items
const items = useMemo(() => sourceItems.filter(...), [sourceItems]);
3. The Stable Callback
const handleSelect = useCallback((id) => { ... }, [onSelect]);
4. The Custom Comparator
memo(Component, (prev, next) => prev.id === next.id)
5. The Children Stable
const children = useMemo(() => <ExpensiveList items={items} />, [items]);
6. The Module-Level Constant
const ITEMS = [{ id: 1, name: 'Alice' }];
7. The Context Consumer
const theme = useContext(ThemeContext);
8. The Display Name
const List = memo(function List({ items }) { ... });
9. The Profiler
// The React DevTools Profiler
10. The Complete Optimized
const items = useMemo(() => sourceItems.filter(...), [sourceItems]);
const handleSelect = useCallback((id) => { ... }, [onSelect]);
<ExpensiveList items={items} onSelect={handleSelect} />
Visual
The Memo Comparison
┌──────────────────────────────────────────────────────────────┐
│ THE MEMO COMPARISON │
│ │
│ The Parent re-renders. │
│ │ │
│ ▼ │
│ The MemoizedList renders. │
│ │ │
│ ▼ │
│ React compares the props: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ prevProps.items === nextProps.items? │ │
│ │ │ │
│ │ YES → the skip (the previous output is reused) │ │
│ │ NO → the render (the new output is computed) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ├── The same reference → the no render │
│ │ │
│ └── The new reference → the render │
│ │
│ The comparison is shallow: the `Object.is` on each prop. │
│ │
└──────────────────────────────────────────────────────────────┘
The Memo Defeaters
┌──────────────────────────────────────────────────────────────┐
│ THE MEMO DEFEATERS │
│ │
│ ❌ THE OBJECT LITERAL: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <List config={{ sortBy: 'name' }} /> │ │
│ │ The new object on every render. │ │
│ │ The reference changes. │ │
│ │ The memo is defeated. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ❌ THE ARRAY LITERAL: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <List items={[...items]} /> │ │
│ │ The new array on every render. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ❌ THE FUNCTION EXPRESSION: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <List onSelect={() => {}} /> │ │
│ │ The new function on every render. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ✅ THE FIX: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const config = useMemo(() => ({ ... }), []); │ │
│ │ const items = useMemo(() => [...], [source]); │ │
│ │ const handleSelect = useCallback(() => {}, []); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Children Defeat
┌──────────────────────────────────────────────────────────────┐
│ THE CHILDREN DEFEAT │
│ │
│ ❌ WITHOUT the useMemo: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ function Parent() { │ │
│ │ return ( │ │
│ │ <MemoizedLayout> │ │
│ │ <ExpensiveList items={items} /> │ │
│ │ </MemoizedLayout> │ │
│ │ ); │ │
│ │ } │ │
│ │ │ │
│ │ The children element is created fresh on every │ │
│ │ render. The reference changes. The memo is defeated.│ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ✅ WITH the useMemo: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ function Parent() { │ │
│ │ const children = useMemo( │ │
│ │ () => <ExpensiveList items={items} />, │ │
│ │ [items] │ │
│ │ ); │ │
│ │ │ │
│ │ return <MemoizedLayout>{children}</MemoizedLayout>;│ │
│ │ } │ │
│ │ │ │
│ │ The children element is stable, and the memo works. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Context Interaction
┌──────────────────────────────────────────────────────────────┐
│ THE CONTEXT INTERACTION │
│ │
│ const ThemeContext = createContext('light'); │
│ │
│ const Button = memo(function Button({ label }) { │
│ const theme = useContext(ThemeContext); │
│ return <button className={theme}>{label}</button>; │
│ }); │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The label is the same. │ │
│ │ The memo compares the label, and the props are equal.│ │
│ │ │ │
│ │ But the theme changed. │ │
│ │ The Button re-renders, because the context changed. │ │
│ │ │ │
│ │ The memo does NOT stop the context re-renders. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The fix: the context splitting, or the selector-based │
│ subscription with the external store. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
React.memo | The higher-order component for the memoization |
| The comparison | The shallow Object.is on each prop |
| The custom comparator | The second argument; the areEqual |
| The memo defeaters | The new object, array, function, children |
| The fix | The useMemo and the useCallback |
| The children | The new element defeats the memo |
| The context | The memo does not stop the context re-renders |
| The trade-off | The comparison cost vs the render cost |
| The measurement | The React DevTools Profiler |
Key takeaways:
React.memomemoizes a component. It wraps the component and skips the re-render when the props are shallowly equal to the previous props. The comparison isObject.ison each top-level prop, and the memo is the optimization for the expensive renders .- The shallow comparison fails for the new references. The object literal, the array literal, and the function expression create the new references on every render, and the memo is defeated. The fix is the
useMemofor the objects and the arrays, and theuseCallbackfor the functions. - The custom comparator customizes the comparison. The second argument to the
memoreceives the previous and the next props, and returnstruefor the equal andfalsefor the different. The deep comparison in the comparator is the anti-pattern, and the key-field comparison is the recommended. - The
childrenprop is the common defeat. Thechildrenelement is created fresh when the parent re-renders, and the reference changes. The fix is theuseMemoto stabilize the children element, or to pass the children as a prop from the stable parent. - The memo does not stop the context re-renders. The memo compares the props, not the context. The component that reads the context re-renders when the context value changes, regardless of the memo. The fix is the context splitting or the selector-based subscription.
- The memo has the cost. The comparison of the props and the memory for the previous props and the output. For the cheap components, the comparison is more expensive than the render. The memo is for the expensive components with the stable props .
- The profiler is the tool. The React DevTools Profiler shows which components re-render and how long they take. The memo should be applied when the profiler shows the problem, not by default .
- The memo and the hooks work together. The parent uses the
useMemoand theuseCallbackto stabilize the props, and the child uses thememoto skip the render when the props are unchanged. The three are the memoization system.
Remember: React.memo is the higher-order component that memoizes a component. It skips the re-render when the props are shallowly equal to the previous props. The comparison is the shallow Object.is, and the new object, array, function, and children references defeat it. The fix is the useMemo and the useCallback in the parent. The memo does not stop the context re-renders, and it has the cost. The result is the optimization for the expensive components with the stable props, which is the foundation of the React performance tuning.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!