When useMemo and useCallback Actually Help
This guide is part of React Rendering Performance, within Framework Performance. useMemo caches the result of a calculation between renders; useCallback caches a function's identity. Both are widely used and widely misused. Wrapped around cheap calculations or callbacks passed to plain DOM elements, they add code, comparison overhead and memory without saving anything. Used in the right places, they prevent expensive recalculation and keep memoized children from re-rendering.
There are exactly three situations where they help: the calculation itself is expensive; the value or function is passed to a child wrapped in React.memo (so a stable identity lets the child skip rendering); or the value is a dependency of an effect or another hook, where a new identity would re-run it. Outside these cases, they are noise. The React Compiler automates most of this, but understanding the rules still matters for code the compiler cannot optimise and for reading profiles.
Rapid Diagnosis
- Profile first: use the React Profiler to find expensive components and why they re-render.
- Time suspicious calculations: wrap them with
console.timein a production-like build. - Check whether children are memoized: stable props only matter for memoized children.
- Look for effects re-running on every render due to object or function dependencies.
Root Cause Analysis
1. Expensive derived data recomputed every render. Sorting, filtering or aggregating large arrays.
2. Memoized children receiving new props. Inline objects and callbacks defeat React.memo.
3. Effects with unstable dependencies. Re-subscribing or refetching on every render.
4. Over-memoization. Memo wrappers everywhere make code harder to read with no gain.
Step-by-Step Resolution
1. Memoize expensive derived data
function OrdersTable({ orders, filter }) {
const visible = useMemo(
() => orders.filter((o) => o.status === filter).sort((a, b) => b.total - a.total),
[orders, filter],
);
return <Table rows={visible} />;
}
// trade-off: useMemo keeps the previous result in memory and compares deps each
// render; worthwhile here because filtering and sorting thousands of rows is slow.
2. Stabilise props for memoized children
const Row = React.memo(function Row({ order, onSelect }) { /* ... */ });
function OrdersTable({ orders }) {
const [selected, setSelected] = useState(null);
const onSelect = useCallback((id) => setSelected(id), []); // stable identity
return orders.map((o) => <Row key={o.id} order={o} onSelect={onSelect} />);
}
// trade-off: without React.memo on Row, useCallback here would do nothing useful.
3. Stabilise effect dependencies
Memoize objects or functions used in effect dependency arrays when a new identity would re-run the effect (re-subscribing, refetching), or move the object creation inside the effect.
4. Remove memoization that does nothing
Delete useMemo around trivial expressions and useCallback for handlers passed to DOM elements (<button onClick>), which do not benefit from stable identities.
Verification
Re-profile the interaction: memoized children should show as not rendered, and expensive calculations should disappear from flame charts on unrelated updates. Check that effects no longer re-run unnecessarily (log or count). Confirm in the Performance panel that interaction time decreased.
Worked Example: An Analytics Dashboard
An analytics dashboard recalculated aggregates for six charts from a 20,000-row dataset on every render, including renders caused by hovering over chart tooltips. Each hover cost 120ms. Wrapping the aggregation in useMemo keyed on the dataset and date range removed the recalculation from hover renders; memoizing the chart components and stabilising their onHover callbacks with useCallback stopped five of six charts re-rendering on each hover. Hover interactions dropped to 8ms. A later code review removed 40 other useMemo and useCallback calls around trivial values, which had no measurable effect on performance and made the code easier to read.
How the React Compiler Changes This
The React Compiler memoizes automatically: it caches JSX elements, computed values and callbacks based on their inputs, at a finer granularity than manual hooks. With the compiler enabled, most manual useMemo, useCallback and React.memo calls become unnecessary (they remain harmless). Manual memoization is still relevant for code the compiler skips (components that break the Rules of React), for very expensive calculations where you want explicit control, and in codebases not yet using the compiler.
Measuring Before and After
Treat memoization like any other optimisation: measure first, change one thing, measure again. In the React Profiler, record the interaction and note the component's render time and how often it renders. Add the memoization, record again, and confirm that the component now shows as not rendered (or renders faster). If nothing changes, remove the memoization — it was not addressing the actual cost. For calculations, wrap them temporarily with performance.now() timing in a production build on a throttled device; a calculation that takes 0.1ms does not need useMemo, however large the array looks in the code.
Common Mistakes
- Memoizing without React.memo on the child. Stable props are pointless if the child always re-renders.
- Wrong dependency arrays. Missing dependencies cause stale values; extra ones cause recomputation.
- Memoizing cheap work. Adds overhead and complexity.
- Using useMemo for semantic guarantees. React may discard cached values; do not rely on it for correctness.
Edge Cases
Context values. Memoize context provider values so consumers do not re-render on every provider render.
Custom hooks. Return stable objects and functions from custom hooks that are used in dependencies elsewhere.
Lists. Memoizing list items helps most when the list re-renders often with mostly unchanged items.
Large objects. Memoization keeps old results alive until dependencies change; be mindful of memory for very large data.
FAQ
Does useCallback make functions faster?
No. It only keeps the same function identity between renders, which matters for memoized children and dependency arrays.
Is useMemo free?
No. It compares dependencies every render and stores results. For cheap calculations, the overhead can exceed the savings.
How expensive must a calculation be to memoize?
A rough threshold is around 1ms or more per render on target devices, or calculations on large collections. Measure with throttling.
Should I memoize context values?
Yes. A provider value created inline changes identity every render and re-renders all consumers.
Do I still need these hooks with the React Compiler?
Mostly not. The compiler memoizes automatically; keep manual hooks only where the compiler cannot optimise or explicit control is needed.
Why does my memoized child still re-render?
One of its props changes identity each render (an inline object, array or callback), or it consumes a context that changes.
Can memoization cause bugs?
Incorrect dependency arrays can produce stale values. The react-hooks/exhaustive-deps lint rule catches most of these mistakes.
Should I memoize event handlers on DOM elements?
No. DOM elements do not skip work based on prop identity, so stable handlers bring no benefit there.
Does useMemo help with expensive child components?
Indirectly. Memoizing a JSX element with useMemo (or wrapping the child in React.memo) lets React skip re-rendering that subtree when its inputs are unchanged.
How do I memoize values in custom hooks?
Use useMemo and useCallback for values and functions the hook returns, so consumers can use them in dependency arrays and memoized children without causing extra work.
Is it bad to memoize everything just in case?
It is rarely harmful to performance in a big way, but it adds code, memory and dependency arrays to maintain. Targeted memoization, or the compiler, gives the benefit with less noise.
Related
- Finding wasted renders with the React Profiler — find where memoization matters.
- Adopting the React Compiler — automatic memoization.
- Stopping context re-render cascades — memoizing context values.