useMemo and useCallback: When They Help and When They Cost You
Memoisation in React has a real price. Here is how to tell which of the three legitimate cases you are in, and why most useMemo calls are net negative.
Table of contents
- Case 1: the computation is actually expensive
- Case 2: referential identity feeds a dependency array
- Case 3: the child is memoised and the prop is a function or object
- Where it goes wrong
- The React Compiler changes the default
- A better first move than memoising
- Frequently asked questions
- Is useCallback just useMemo for functions?
- Does useMemo guarantee the value is cached?
- Should I memoise context values?
- How do I know if memoisation helped?
- Related reading
- References
useMemo and useCallback are not free. Each one adds a closure, a dependency array to compare on every render, and a cache entry to retain. Applied indiscriminately they make an app slower and harder to read.
There are three situations where they genuinely pay off.
Case 1: the computation is actually expensive#
// Worth memoising: O(n log n) over thousands of rows
const sorted = useMemo(() => [...rows].sort((a, b) => b.score - a.score), [rows]);
// Not worth memoising: this is cheaper than the comparison
const label = useMemo(() => `${first} ${last}`, [first, last]);The threshold is roughly "would this show up in a profile?". String concatenation, arithmetic and small array operations never do. Sorting or filtering tens of thousands of items, parsing, or building a large lookup map do.
Case 2: referential identity feeds a dependency array#
This is the case people miss, and it is a correctness issue rather than a performance one:
// New object every render → the effect re-runs every render
const options = { includeArchived: true, sort: 'name' };
useEffect(() => {
fetchItems(options);
}, [options]); // ← never equal to the previous optionsMemoising fixes the loop:
const options = useMemo(() => ({ includeArchived, sort }), [includeArchived, sort]);The same applies to a useCallback passed into a custom hook's dependency array.
Case 3: the child is memoised and the prop is a function or object#
const Row = memo(function Row({ item, onSelect }) {
/* ... */
});
function List({ items }) {
// Without useCallback, onSelect is a new function each render, so every
// memo() child re-renders anyway and the memo is pure overhead.
const handleSelect = useCallback((id) => setSelected(id), []);
return items.map((item) => <Row key={item.id} item={item} onSelect={handleSelect} />);
}Note the dependency: memo on the child and a stable prop must both be present. Either alone does nothing.
Where it goes wrong#
Memoising everything. A component with fifteen useMemo calls pays fifteen dependency-array comparisons per render to avoid work that cost less than the comparisons.
Dependency arrays that always change. If a dependency is itself recreated each render, the memo never hits and you have added cost for zero benefit. Memoisation has to go all the way down or not at all — which is a good argument for restructuring instead.
Reaching for memo before measuring. Slow React apps are usually slow because too many components re-render or because a list is not virtualised, not because a callback was recreated. Profile first.
The React Compiler changes the default#
The React Compiler memoises automatically at build time, correctly, without you annotating anything. In a project using it, hand-written useMemo and useCallback become mostly redundant and the remaining reason to write them is Case 2 — where you need a guaranteed stable reference for an external API.
If you are on the compiler, the right move is to stop adding new memo calls rather than to rip out existing ones.
A better first move than memoising#
Restructuring often removes the need entirely:
// Expensive parent re-renders on every keystroke
function Page() {
const [query, setQuery] = useState('');
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ExpensiveChart data={data} />
</>
);
}
// Move the state down: the chart is no longer in the re-rendering subtree
function Page() {
return (
<>
<SearchBox />
<ExpensiveChart data={data} />
</>
);
}No memoisation, strictly less work, less code.
Frequently asked questions#
Is useCallback just useMemo for functions?#
Effectively, yes — useCallback(fn, deps) is useMemo(() => fn, deps). The separate hook exists for readability.
Does useMemo guarantee the value is cached?#
No. React may discard the cache — for example to free memory. useMemo is a performance hint, not a semantic guarantee, so never rely on it for correctness of side effects.
Should I memoise context values?#
Yes, this is a real case. A context provider whose value is a fresh object each render re-renders every consumer. Memoise it.
How do I know if memoisation helped?#
React DevTools Profiler. Record an interaction before and after; look at the flamegraph for components that stopped re-rendering. If nothing changed, revert it.
Related reading#
- React Hooks Guide
- React Performance Optimization
- JavaScript Closures — why stale values happen in dependency arrays