useMemo vs useCallback
useMemo and useCallback are sibling performance hooks — both cache something between renders and both use a dependency array. The difference is simple: useMemo caches a value, useCallback caches a function. In fact, useCallback is just syntactic sugar for a specific use of useMemo.
The Fundamental Equivalence
// These two are exactly equivalent: const memoizedFn = useCallback(fn, deps) const memoizedFn = useMemo(() => fn, deps) // useCallback is shorthand for "memoize this function reference" // useMemo is the general form: "memoize whatever this factory returns"
React's source code implements useCallback(fn, deps) as useMemo(() => fn, deps). The distinction is entirely about clarity of intent: when you see useCallback, you know the author is memoizing a function; when you see useMemo, you know they are memoizing a computed value.
useMemo: Memoizing a Computed Value
useMemo is designed for cases where you need to cache the result of calling a function. The factory function runs and its return value is stored.
import { useMemo } from 'react'
function ProductSearch({ products, query, minPrice }) {
// useMemo caches the *result* (the filtered array)
const results = useMemo(() => {
return products
.filter(p => p.name.toLowerCase().includes(query.toLowerCase()))
.filter(p => p.price >= minPrice)
.sort((a, b) => a.price - b.price)
}, [products, query, minPrice])
return <ul>{results.map(p => <li key={p.id}>{p.name} — ${p.price}</li>)}</ul>
}useCallback: Memoizing a Function
useCallback is designed for cases where you need to cache a function reference — not its return value — so that the reference stays stable across renders.
import { useCallback } from 'react'
function ProductSearch({ products, query, minPrice, onSelect }) {
// useCallback caches the *function itself* (stable reference)
const handleSelect = useCallback((product) => {
console.log('Selected:', product)
onSelect(product)
}, [onSelect])
// 'handleSelect' is the same reference as long as 'onSelect' is the same
return <ProductList onSelect={handleSelect} />
}The Same Problem — Solved with Each
Consider a DataGrid that shows a sorted, filtered table. It has an expensive Row child. We need to:
Memoize the sorted and filtered data (computed value) →
useMemoMemoize the row click handler (function) →
useCallback
import { useState, useMemo, useCallback, memo } from 'react'
const Row = memo(function Row({ item, onSelect }) {
return (
<tr onClick={() => onSelect(item)}>
<td>{item.name}</td>
<td>{item.value}</td>
</tr>
)
})
function DataGrid({ data, filter, onItemSelect }) {
const [sortKey, setSortKey] = useState('name')
// ✅ useMemo — caches the computed/transformed array (a value)
const processedData = useMemo(() => {
const filtered = data.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
)
return [...filtered].sort((a, b) => {
if (a[sortKey] < b[sortKey]) return -1
if (a[sortKey] > b[sortKey]) return 1
return 0
})
}, [data, filter, sortKey]) // recompute when any of these change
// ✅ useCallback — caches the function reference (so Row.memo works)
const handleSelect = useCallback((item) => {
console.log('Row clicked:', item.id)
onItemSelect(item)
}, [onItemSelect]) // only changes when onItemSelect changes
return (
<div>
<button onClick={() => setSortKey('name')}>Sort by Name</button>
<button onClick={() => setSortKey('value')}>Sort by Value</button>
<table>
<tbody>
{processedData.map(item => (
<Row key={item.id} item={item} onSelect={handleSelect} />
))}
</tbody>
</table>
</div>
)
}The Decision Table
What you want to cache | Hook to use | What gets stored |
|---|---|---|
Result of filtering/sorting a list |
| The filtered/sorted array |
Result of a calculation |
| The computed number/string/object |
An object/array passed as a stable prop |
| The object/array |
An event handler for a React.memo child |
| The function itself |
A function used in useEffect deps |
| The function itself |
A ref callback |
| The function itself |
Cost of Memoization
Both hooks have a non-zero overhead on every render:
Memory allocation to store the cached value/function
Dependency comparison on every render (using
Object.is)Cache invalidation logic managed by React
For small arrays, simple functions, and infrequently-rendered components, the overhead of memoization often exceeds the savings.
The Golden Rule
Before adding either hook, ask yourself three questions:
Have I measured a performance problem? Use the React Profiler to confirm the component is actually slow.
Is this the bottleneck? The Profiler shows which components are expensive and why.
Will memoization actually help? Only if the computation is truly expensive (useMemo) or the recipient is wrapped with React.memo (useCallback).
Quick Reference
// useMemo: cache a VALUE
const expensiveResult = useMemo(() => computeExpensiveValue(a, b), [a, b])
const stableObject = useMemo(() => ({ x: a, y: b }), [a, b])
const filteredList = useMemo(() => items.filter(pred), [items, pred])
// useCallback: cache a FUNCTION
const stableHandler = useCallback((arg) => doSomething(arg, dep), [dep])
const stableFetcher = useCallback(() => fetch(url), [url])
// Remember: useCallback(fn, deps) === useMemo(() => fn, deps)