The Memoization Trap
Junior React developers don't use useMemo and useCallback. Senior React developers overuse them. The goal is to use them exactly when they help.
The key insight: React is already fast. Unnecessary renders are rarely the problem.
What These Hooks Actually Do
// useMemo: memoize a computed value
const sortedItems = useMemo(() => {
return items.slice().sort(compareFn)
}, [items, compareFn])
// useCallback: memoize a function reference
const handleClick = useCallback(() => {
doSomething(id)
}, [id])
Both cache their result until their dependencies change. This is useful only if:
- The thing being cached is expensive to compute, OR
- The cached thing is passed to a component that checks reference equality (i.e., it's wrapped in
React.memo)
If neither condition is true, you're adding overhead with no benefit.
When NOT to Use useMemo
// ❌ Pointless memoization — filtering a small array is free
const filteredItems = useMemo(() => {
return items.filter(i => i.active)
}, [items])
// ✅ Just do it
const filteredItems = items.filter(i => i.active)
// ❌ Pointless — object creation is cheap
const style = useMemo(() => ({
color: 'red',
fontSize: '16px',
}), [])
// ✅ Move it outside the component or just inline it
const STYLE = { color: 'red', fontSize: '16px' }
The memoization itself has a cost: React has to store the cached value and compare dependencies on every render. For cheap operations, this overhead exceeds the savings.
When NOT to Use useCallback
// ❌ Pointless — Parent re-renders anyway, ChildComponent isn't memo'd
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked')
}, [])
return <ChildComponent onClick={handleClick} /> // Not React.memo'd
}
If ChildComponent isn't wrapped in React.memo, it re-renders when Parent re-renders regardless. The stable function reference from useCallback does nothing.
When TO Use useMemo
Expensive computation
// ✅ Sorting/filtering large datasets is expensive
const processedData = useMemo(() => {
return rawData
.filter(item => item.date > cutoff)
.sort((a, b) => b.value - a.value)
.slice(0, 100)
}, [rawData, cutoff])
Stable reference for React.memo'd components
const MemoChild = React.memo(({ config }) => <ExpensiveChart config={config} />)
function Parent({ data }) {
// ✅ Without useMemo, config is a new object every render
// React.memo sees a different reference and re-renders
const config = useMemo(() => ({
data,
theme: 'dark',
animations: true,
}), [data])
return <MemoChild config={config} />
}
When TO Use useCallback
Stable callback for React.memo'd children
const ListItem = React.memo(({ item, onDelete }) => (
<div>
{item.name}
<button onClick={onDelete}>Delete</button>
</div>
))
function List({ items }) {
// ✅ Without useCallback, onDelete is a new function every render
// Each ListItem re-renders even though nothing changed for it
const handleDelete = useCallback((id) => {
deleteItem(id)
}, [])
return items.map(item =>
<ListItem key={item.id} item={item} onDelete={() => handleDelete(item.id)} />
)
}
Stable dependency for other hooks
// ✅ Without useCallback, fetchData changes every render
// useEffect would re-run on every render
const fetchData = useCallback(async () => {
const data = await api.get('/data')
setData(data)
}, [])
useEffect(() => {
fetchData()
}, [fetchData])
The Decision Framework
Before adding useMemo or useCallback, ask:
- Is this computation actually expensive? Profile it. Don't guess.
- Is the result passed to a
React.memo'd component? If not, stable reference doesn't help. - Is this a
useEffectdependency that needs to be stable? ThenuseCallbackis appropriate.
If none of these are true: skip it.
React.memo: Use It Sparingly Too
// ✅ Good use of React.memo: expensive component, props rarely change
const DataGrid = React.memo(({ rows, columns, onSort }) => {
// Renders thousands of cells
return <ComplexGrid rows={rows} columns={columns} onSort={onSort} />
})
// ❌ Pointless React.memo: cheap component, props change constantly
const Badge = React.memo(({ count }) => <span>{count}</span>)
The Real Performance Wins
Before reaching for useMemo/useCallback:
- Move to Server Components — don't render it on the client at all
- Code split — lazy load heavy components
- Virtualize long lists —
react-windoworreact-virtual - Fix the real bottleneck — profile with React DevTools Profiler first
Key Takeaways
useMemo/useCallbackadd overhead — they help only in specific scenarios- They're useful for: expensive computations, stable refs for
React.memocomponents, stableuseEffectdependencies - Wrap a child in
React.memoonly if it's expensive AND its props are stable - Always profile before optimizing — React is fast and you might be solving the wrong problem
- Server Components are the best performance optimization for data-heavy UI