React.memo
By default, whenever a React component re-renders, all of its children re-render too — even if their props haven't changed. For small, fast components this is fine. But when a parent re-renders frequently and a child is expensive to render, those extra renders waste CPU time. React.memo is the tool for breaking that chain: it wraps a component and skips the re-render when props are identical.
The Basic API
import { memo } from 'react'
// Unwrapped — re-renders every time the parent re-renders
function ExpensiveList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
// Wrapped — only re-renders when 'items' changes (shallow equality)
const ExpensiveList = memo(function ExpensiveList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
})
// Arrow function syntax
const ExpensiveList = memo(({ items }) => (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
))How Shallow Equality Works
By default, React.memo uses shallow equality: it compares each prop with Object.is. Primitive values (strings, numbers, booleans) compare by value. Objects and arrays compare by reference. This distinction is critical:
const Child = memo(({ user, onClick }) => {
console.log('Child rendered')
return <button onClick={onClick}>{user.name}</button>
})
function Parent() {
const [count, setCount] = useState(0)
// ✗ New object reference on every render — memo is defeated
const user = { name: 'Alice', id: 1 }
// ✗ New function reference on every render — memo is defeated
const handleClick = () => console.log('clicked')
return (
<>
<button onClick={() => setCount(c => c + 1)}>Parent: {count}</button>
{/* Child re-renders on every parent render because user and handleClick
are new references each time */}
<Child user={user} onClick={handleClick} />
</>
)
}The Fix: useMemo + useCallback
To make React.memo effective, stabilize the prop references with useMemo (for objects/arrays) and useCallback (for functions):
import { memo, useState, useMemo, useCallback } from 'react'
const Child = memo(({ user, onClick }) => {
console.log('Child rendered')
return <button onClick={onClick}>{user.name}</button>
})
function Parent() {
const [count, setCount] = useState(0)
// ✓ Same reference between renders (unless id changes)
const user = useMemo(() => ({ name: 'Alice', id: 1 }), [])
// ✓ Same function reference between renders
const handleClick = useCallback(() => console.log('clicked'), [])
return (
<>
<button onClick={() => setCount(c => c + 1)}>Parent: {count}</button>
{/* Now Child does NOT re-render when count changes */}
<Child user={user} onClick={handleClick} />
</>
)
}Custom Comparison Function
The second argument to React.memo is a comparison function. It receives the previous and next props and returns true to skip the re-render, or false to allow it. Think of it as the inverse of shouldComponentUpdate:
const UserCard = memo(
function UserCard({ user, highlightColor }) {
return (
<div style={{ background: highlightColor }}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)
},
// Custom comparison: only re-render if user.id or highlightColor changes
// (ignore changes to other user properties like lastSeen, etc.)
(prevProps, nextProps) => {
const sameUser = prevProps.user.id === nextProps.user.id
const sameColor = prevProps.highlightColor === nextProps.highlightColor
// return true → SKIP re-render (props are "equal enough")
// return false → ALLOW re-render
return sameUser && sameColor
}
)When React.memo Helps
Parent re-renders frequently for reasons unrelated to the child (a text input updating, a timer ticking, an animation running).
Child renders are expensive — long lists, complex SVGs, heavy calculations in the render body.
Props are stable — primitive values, or objects/functions stabilized with
useMemo/useCallback.Pure presentational leaf components — no state, no side effects, just props → JSX.
When React.memo Doesn't Help
Props always change — if every render produces different prop values, memo just adds the overhead of comparison for no benefit.
The component is fast — if the render takes under 1 ms, the memo comparison overhead is comparable to just re-rendering.
The component reads changing context —
React.memodoes not prevent context-driven re-renders.State or children — memo only checks props; if the wrapped component has its own
useState, state changes still trigger re-renders.
A Practical Example: Row in a Large List
import { memo, useState, useCallback } from 'react'
// Individual row — potentially re-created 1000 times per parent render without memo
const TodoRow = memo(function TodoRow({ todo, onToggle }) {
return (
<li
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
onClick={() => onToggle(todo.id)}
>
{todo.text}
</li>
)
})
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Walk the dog', done: true },
// ... imagine 1000 items
])
const [filter, setFilter] = useState('all')
// Stable callback — does not change between renders
const handleToggle = useCallback((id) => {
setTodos(prev =>
prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
)
}, [])
const visible = todos.filter(t =>
filter === 'all' || (filter === 'done') === t.done
)
return (
<>
{/* Changing the filter causes TodoList to re-render, but each TodoRow
only re-renders if its own todo object changed */}
<select value={filter} onChange={e => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="done">Done</option>
<option value="pending">Pending</option>
</select>
<ul>
{visible.map(todo => (
<TodoRow key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</ul>
</>
)
}Quick Reference
memo(Component)— skip re-render when props are shallowly equal.memo(Component, (prev, next) => bool)— custom equality; returntrueto skip.Inline objects/arrays/functions defeat memo — stabilize with
useMemo/useCallback.Memo only checks props — state, ref, and context changes still cause re-renders.
Profile first. Only memoize components that are measurably slow.