Lesson 23 of 31
useMemo and useCallback
Memoizing an expensive calculation and memoizing a function's identity across renders — what each buys you, and why reaching for either without measuring is often premature.
React.memo skips a child's re-render when its props are unchanged — but
that only works if the props reaching it are actually the same reference
when nothing relevant changed. useMemo and useCallback are the two tools
for making that true.
useMemo: memoizing a value
import { useMemo } from "react";
function ProductList({ products, filterText }) {
const filtered = useMemo(() => {
console.log("Filtering products...");
return products.filter((p) => p.name.includes(filterText));
}, [products, filterText]);
return <ul>{filtered.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}Without useMemo, products.filter(...) would re-run on every render of
ProductList — including renders triggered by something completely
unrelated to products or filterText. useMemo re-runs the calculation
only when one of the listed dependencies has actually changed since the
last render, and returns the previously cached result otherwise. The
dependency array works exactly like useEffect's — same comparison rules,
same "list everything the calculation reads" discipline.
This also solves the reference-stability problem directly: filtered is
now the same array reference across renders where products and
filterText haven't changed, which matters if filtered is later passed
as a prop to a React.memo-wrapped child.
useCallback: memoizing a function's identity
useCallback is the same idea, specialized for functions:
import { useCallback } from "react";
function ProductList({ products, onSelect }) {
const handleSelect = useCallback(
(id) => {
onSelect(id);
console.log("Selected", id);
},
[onSelect]
);
return <MemoizedRow products={products} onSelect={handleSelect} />;
}Without useCallback, handleSelect would be a brand-new function on every
render of ProductList — a new reference every time, even though its
behavior never changes. If MemoizedRow is wrapped in React.memo, that
fresh reference would defeat the memoization entirely (from the previous
lesson's exact failure case), since the shallow prop comparison would see a
"changed" onSelect every single render. useCallback(fn, deps) is really
just useMemo(() => fn, deps) — it's not calling the function or caching
its result, only caching the function reference itself.
The two exist to work together
The realistic pattern combines all three tools from this and the previous lesson:
const MemoizedRow = memo(function Row({ product, onSelect }) {
return <li onClick={() => onSelect(product.id)}>{product.name}</li>;
});
function ProductList({ products, onSelect }) {
const stableOnSelect = useCallback((id) => onSelect(id), [onSelect]);
return (
<ul>
{products.map((p) => (
<MemoizedRow key={p.id} product={p} onSelect={stableOnSelect} />
))}
</ul>
);
}React.memo on Row only pays off because stableOnSelect keeps the same
reference across renders where onSelect itself hasn't changed — without
useCallback here, every row would still re-render on every list render,
memo or not.
Both have a real cost — this is not a free win
useMemo and useCallback are not without cost: React still has to store
the previous dependencies and compare them on every render, and for a
genuinely cheap calculation (adding two numbers, formatting a short string)
or a function with no memoized descendant depending on its stability, that
bookkeeping is pure overhead with no skipped work to show for it. The
practical guidance:
- Reach for
useMemowhen a calculation is measurably expensive (looping over a large array, complex math) — not for trivial arithmetic. - Reach for
useCallbackspecifically when the function is passed to aReact.memo-wrapped component, or into another hook's dependency array where reference stability matters. - When in doubt, don't add either preemptively — a profiler showing an actual slow render is a better reason to memoize than a general instinct that "this might help."
Try it yourself
What to remember
- useMemo caches a calculation's result, re-running it only when its dependencies change; useCallback caches a function's identity the same way.
- Both solve the same underlying problem: giving a value or function a stable reference across renders, which is what lets React.memo (or another hook's dependency array) actually detect "nothing changed."
- An inline function passed to a memoized component defeats the memoization, because it's a new reference every render — useCallback is the fix.
- Both have a real per-render cost; reserve them for measurably expensive calculations or for stabilizing props into memoized children, not as a reflexive default.
Check yourself
4 questions · pass 3/4 to unlock Keys and List Reconciliation Pitfalls
1.What does useMemo actually memoize?
2.What does useCallback memoize, as distinct from useMemo?
3.Why would passing an unmemoized inline function as a prop defeat a child wrapped in React.memo?
4.Why is 'wrap every calculation in useMemo and every function in useCallback' not automatically good practice?
4 left to answer