useMemo vs useCallback in React — when each one actually helps
Both are caching hooks — they remember a value between renders so React doesn't recompute or recreate it. They differ in what they cache.
| Caches | Returns | |
|---|---|---|
useMemo | Result of a function | The value |
useCallback | The function itself | The function |
useMemo — cache an expensive value
const sortedItems = useMemo(
() => items.slice().sort((a, b) => b.score - a.score),
[items], // only re-sort when items change
);
Without useMemo, every parent re-render re-sorts a potentially huge array.
useCallback — cache a function reference
const handleClick = useCallback((id) => {
setSelected(id);
}, []);
return <Child onClick={handleClick} />;
Without useCallback, every render creates a new handleClick function. If Child is wrapped in React.memo, that new reference defeats the memoization and Child re-renders anyway.
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)
useCallback((x) => x + 1, [])
// is exactly the same as
useMemo(() => (x) => x + 1, [])
Different sugar for the same idea.
When they actually help — short list
- The computation is genuinely expensive (sorting 10k items, parsing markdown, heavy math).
- The value/function is passed to a
React.memo'd child where reference stability matters. - The value/function is in a
useEffectdep array and you want to control when the effect re-runs.
When they hurt — much more common
// ❌ Adds memory + hash overhead for zero benefit
const sum = useMemo(() => a + b, [a, b]);
const onClick = useCallback(() => console.log('hi'), []);
If the computation is cheap (a few add/multiply ops, a small array map), the bookkeeping of useMemo costs more than the function itself. Just write const sum = a + b;.
How to tell if it helps
Use the React DevTools Profiler. Measure render duration before and after. If a component's render time doesn't drop noticeably, the memo is dead weight.
React.memo + useCallback pattern
const Row = React.memo(function Row({ item, onSelect }) {
return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});
function List({ items }) {
// ✅ stable reference — React.memo can actually skip re-renders
const handleSelect = useCallback((id) => doStuff(id), []);
return items.map((it) => <Row key={it.id} item={it} onSelect={handleSelect} />);
}
Without useCallback here, Row re-renders every time List does, even though nothing it cares about changed.
Default position — don't memoize
Modern React is fast. Memoize when:
- The Profiler shows the component is a perf bottleneck
- A
React.memochild is re-rendering because of a fresh prop reference - The computation is measurably expensive
Otherwise it's noise.
React Compiler (forthcoming)
React 19's experimental compiler memoizes automatically. Once stable, manual useMemo / useCallback mostly become unnecessary — but until then, profile-driven manual memoization is the rule.