useMemo and useCallback Correctly
Memoization in React Has One Purpose: Referential Equality
Here's the thing most people get wrong about these hooks: they reach for useMemo and useCallback to "make things faster." That's usually the wrong reason. These hooks exist to preserve referential equality — ensuring that the same object/function reference is reused across renders so that downstream consumers (React.memo, useEffect dependencies, context values) do not trigger unnecessary work.
// Without useMemo — new array every render
const filtered = items.filter(i => i.active);
// Object.is(prevFiltered, nextFiltered) → false (always)
// With useMemo — same array when items and filter do not change
const filtered = useMemo(() => items.filter(i => i.active), [items]);
// Object.is(prevFiltered, nextFiltered) → true (when items is same)
Think of useMemo and useCallback as sticky labels on values. Without a label, React creates a new value every render and throws the old one away. With a label, React keeps the old value and checks: "did the ingredients (dependencies) change? No? Keep the labeled value. Yes? Make a new one and re-label it." The label is only useful if something downstream cares about identity — React.memo, useEffect deps, or context values. If nothing checks identity, the label is wasted effort.
useMemo: Caching Computed Values
const expensiveResult = useMemo(() => {
return heavyComputation(data);
}, [data]);
useMemo calls the function and caches its return value. It only re-computes when a dependency changes.
When useMemo Actually Helps
1. Expensive computations with the same inputs:
function Dashboard({ transactions }) {
// Sorting 10,000 transactions on every render is wasteful
const sorted = useMemo(
() => [...transactions].sort((a, b) => b.date - a.date),
[transactions]
);
return <TransactionList transactions={sorted} />;
}
2. Preserving reference for React.memo children:
const MemoizedList = React.memo(function List({ items }) {
return items.map(item => <li key={item.id}>{item.name}</li>);
});
function Parent() {
const [query, setQuery] = useState('');
const [items, setItems] = useState(allItems);
// Without useMemo, filtered is a new array every render.
// MemoizedList re-renders because items prop changed (new reference).
const filtered = useMemo(
() => items.filter(i => i.name.includes(query)),
[items, query]
);
return <MemoizedList items={filtered} />;
}
3. Stabilizing context values:
function ThemeProvider({ children }) {
const [mode, setMode] = useState('light');
const value = useMemo(() => ({
mode,
toggle: () => setMode(m => m === 'light' ? 'dark' : 'light'),
}), [mode]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
useCallback: Caching Function References
useCallback(fn, deps) is syntactic sugar for useMemo(() => fn, deps):
// These are equivalent:
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
const handleClick = useMemo(() => {
return () => setCount(c => c + 1);
}, []);
When useCallback Actually Helps
1. Passing callbacks to React.memo children:
const MemoizedButton = React.memo(function Button({ onClick, label }) {
console.log('Button rendered:', label);
return <button onClick={onClick}>{label}</button>;
});
function Toolbar() {
const [count, setCount] = useState(0);
// Without useCallback: new function every render → MemoizedButton re-renders
const handleSave = useCallback(() => {
save();
}, []);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<MemoizedButton onClick={handleSave} label="Save" />
</div>
);
}
2. As useEffect dependencies:
function Chat({ roomId }) {
const createConnection = useCallback(() => {
return new WebSocket(`wss://chat.example.com/${roomId}`);
}, [roomId]);
useEffect(() => {
const ws = createConnection();
return () => ws.close();
}, [createConnection]); // Stable when roomId is stable
}
When NOT to Use Them
Wrapping every function in useCallback and every value in useMemo is a common anti-pattern. Memoization is not free — React must store the previous value, compare dependencies on every render, and manage the cache. If nothing downstream checks referential equality, memoization adds cost without benefit.
// UNNECESSARY — no consumer checks referential equality
function Component() {
// No React.memo child, no useEffect dep, no context value
const handleClick = useCallback(() => {
doSomething();
}, []); // Wasted — no one benefits from this being stable
return <button onClick={handleClick}>Click</button>;
}
// UNNECESSARY — the computation is trivial
function Component({ items }) {
const count = useMemo(() => items.length, []); // Overkill
// items.length is O(1) — cheaper than the useMemo overhead itself
}
// UNNECESSARY — primitive values are already stable
function Component({ name }) {
const greeting = useMemo(() => `Hello, ${name}`, [name]);
// String concatenation is instant. useMemo adds overhead for nothing.
}
The Decision Framework
Ask these questions before adding useMemo or useCallback:
- Is the value passed to a React.memo component? → Maybe useMemo/useCallback
- Is the value in a useEffect dependency array? → Maybe useMemo/useCallback
- Is the value a context Provider value? → Probably useMemo
- Is the computation genuinely expensive (>1ms)? → Maybe useMemo
- None of the above? → Do not memoize
Production Scenario: Data Table with Memoized Derived State
const MemoizedRow = React.memo(function Row({ item, onDelete }) {
return (
<tr>
<td>{item.name}</td>
<td>{item.price}</td>
<td><button onClick={() => onDelete(item.id)}>Delete</button></td>
</tr>
);
});
function DataTable({ items, sortField, filterQuery }) {
const processedItems = useMemo(() => {
let result = items;
if (filterQuery) {
result = result.filter(i =>
i.name.toLowerCase().includes(filterQuery.toLowerCase())
);
}
result = [...result].sort((a, b) =>
a[sortField] > b[sortField] ? 1 : -1
);
return result;
}, [items, sortField, filterQuery]);
const handleDelete = useCallback((id) => {
deleteItem(id);
}, []);
return (
<table>
<tbody>
{processedItems.map(item => (
<MemoizedRow
key={item.id}
item={item}
onDelete={handleDelete}
/>
))}
</tbody>
</table>
);
}
The React Compiler makes this automatic
The React Compiler (React Forget) auto-memoizes components and hooks at compile time. It analyzes data flow and inserts memoization where it detects referential equality would prevent unnecessary work. When the Compiler is widely adopted, manual useMemo and useCallback will largely become unnecessary. But understanding why they exist — referential equality — remains essential for debugging and for codebases without the Compiler.
| What developers do | What they should do |
|---|---|
| Wrapping every function in useCallback 'for performance' useCallback has overhead: storing previous function, comparing deps. Without a consumer that checks referential equality, it is pure cost. | Only use useCallback when the function is passed to React.memo children, used in useEffect deps, or is a context value |
| Using useMemo for trivial computations: useMemo(() => a + b, [a, b]) The useMemo overhead (dep comparison, cache management) exceeds the cost of simple arithmetic. Memoize only expensive computations. | Just compute directly: const sum = a + b |
| Adding useMemo/useCallback to fix a performance problem you have not measured Premature memoization adds complexity. Profile to find what actually causes slow renders, then optimize specifically. | Profile first with React DevTools Profiler. Then memoize the measured bottleneck. |
| Expecting useMemo to be a deep cache (like lodash.memoize) Alternating between two different dependency sets causes recomputation on every render. useMemo is not a multi-entry cache. | useMemo caches only ONE previous result — the most recent computation |
- 1useMemo and useCallback exist for referential equality — not for making things faster by default
- 2useCallback(fn, deps) === useMemo(() => fn, deps) — they are the same mechanism
- 3Only memoize when: passed to React.memo child, used as useEffect dep, or used in context value
- 4useMemo caches exactly ONE result — not an LRU cache or multi-entry cache
- 5The React Compiler will auto-memoize — understanding why matters more than when
Challenge: Optimize the Filtered List
Challenge: Strategic Memoization
// This component re-renders every child on every keystroke
// even though only the filtered list changes. Add memoization
// in the right places — and ONLY the right places.
function App() {
const [query, setQuery] = useState('');
const [items] = useState([
{ id: 1, name: 'React' },
{ id: 2, name: 'Vue' },
{ id: 3, name: 'Angular' },
{ id: 4, name: 'Svelte' },
]);
const filtered = items.filter(i =>
i.name.toLowerCase().includes(query.toLowerCase())
);
const handleDelete = (id) => {
console.log('Delete', id);
};
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<Stats count={filtered.length} />
{filtered.map(item => (
<ItemCard key={item.id} item={item} onDelete={handleDelete} />
))}
</div>
);
}
function Stats({ count }) {
console.log('Stats rendered');
return <p>`{count}` results</p>;
}
function ItemCard({ item, onDelete }) {
console.log('ItemCard rendered:', item.name);
return (
<div>
`{item.name}`
<button onClick={() => onDelete(item.id)}>X</button>
</div>
);
}
Show Answer
const MemoizedStats = React.memo(Stats);
const MemoizedItemCard = React.memo(ItemCard);
function App() {
const [query, setQuery] = useState('');
const [items] = useState([
{ id: 1, name: 'React' },
{ id: 2, name: 'Vue' },
{ id: 3, name: 'Angular' },
{ id: 4, name: 'Svelte' },
]);
const filtered = useMemo(
() => items.filter(i =>
i.name.toLowerCase().includes(query.toLowerCase())
),
[items, query]
);
const handleDelete = useCallback((id) => {
console.log('Delete', id);
}, []);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<MemoizedStats count={filtered.length} />
{filtered.map(item => (
<MemoizedItemCard key={item.id} item={item} onDelete={handleDelete} />
))}
</div>
);
}Changes and reasons:
React.memo(Stats)— prevents re-render when count has not changedReact.memo(ItemCard)— prevents re-render when item and onDelete are stableuseMemoon filtered — ensures the same array reference when query/items are the same (benefits ItemCard memo)useCallbackon handleDelete — stable reference so MemoizedItemCard does not re-render due to changed onDelete prop
Without the React.memo wrappers, useMemo and useCallback would be pointless.