useMemo: Memoizing Expensive Calculations
useMemo does two different jobs — and knowing which one you need makes the difference between a useful optimization and noise.
useMemo is the most frequently misused memoization hook. I've seen it wrapping values that don't need it and missing from places where it actually matters. The key is that useMemo solves two distinct problems — and understanding both clarifies when to reach for it.
The API
const memoizedValue = useMemo(
() => computeExpensiveValue(a, b),
[a, b] // dependency array
);useMemo takes a factory function and a dependency array. On the first render, it calls the factory and stores the result. On subsequent renders, it checks the dependencies — if they haven't changed, it returns the cached result. If they have changed, it calls the factory again and updates the cache.
The key difference from useCallback: useMemo memoizes the return value of the function. useCallback memoizes the function itself.
// useCallback returns the function
const fn = useCallback(() => computeValue(a), [a]);
// useMemo returns the result of calling the function
const value = useMemo(() => computeValue(a), [a]);Use Case 1: Expensive Calculations
The obvious use case — avoid re-running a slow computation on every render.
function ProductList({ products, filters }) {
// Without useMemo: runs on every render, even when only unrelated state changes
const filteredProducts = products
.filter(p => p.category === filters.category)
.filter(p => p.price <= filters.maxPrice)
.sort((a, b) => a.price - b.price);
return filteredProducts.map(p => <ProductCard key={p.id} product={p} />);
}With a large product list, this filter + sort runs on every render — even if the user just scrolled and some animation state changed.
function ProductList({ products, filters }) {
// Only recalculates when products or filters change
const filteredProducts = useMemo(() =>
products
.filter(p => p.category === filters.category)
.filter(p => p.price <= filters.maxPrice)
.sort((a, b) => a.price - b.price),
[products, filters]
);
return filteredProducts.map(p => <ProductCard key={p.id} product={p} />);
}Important caveat: measure before adding useMemo here. Most JavaScript array operations on reasonably-sized datasets are fast — often under a millisecond. The rule from Two Rules of React Performance applies here: measure first, then optimize. useMemo adds overhead on every render (the dependency check). For fast calculations, that overhead can outweigh the savings.
A good heuristic: if the computation takes less than 1ms in the profiler, useMemo is not worth it.
Use Case 2: Stable Object References
This is the use case people reach for less often, but it's equally important.
As covered in React.memo and Referential Equality, object props break React.memo because a new object is created on every render — even with identical content.
useMemo fixes this by returning the same object reference until the dependencies change:
function UserDashboard({ userId, theme }) {
// Without useMemo — new object every render → breaks any memo'd children
const config = { userId, theme, timestamp: Date.now() };
// With useMemo — same reference until userId or theme changes
const config = useMemo(
() => ({ userId, theme, timestamp: Date.now() }),
[userId, theme]
);
return <Dashboard config={config} />;
}This also applies to arrays passed as props:
function Chart({ dataPoints, activeIds }) {
// Without useMemo — new array reference every render
const filteredData = dataPoints.filter(d => activeIds.includes(d.id));
// With useMemo — same reference if dataPoints and activeIds haven't changed
const filteredData = useMemo(
() => dataPoints.filter(d => activeIds.includes(d.id)),
[dataPoints, activeIds]
);
return <LineChart data={filteredData} />;
}Dependency Arrays and Stale Values
The dependency array is the same story as useCallback — anything the factory function closes over needs to be in the array. Omitting a dependency gives you stale data that doesn't update when it should.
function SearchResults({ query, pageSize }) {
// ❌ If pageSize changes, results won't update — pageSize missing from deps
const results = useMemo(
() => search(query).slice(0, pageSize),
[query] // pageSize is missing
);
// ✅ Correct
const results = useMemo(
() => search(query).slice(0, pageSize),
[query, pageSize]
);
}ESLint's react-hooks/exhaustive-deps rule catches these — it's worth having enabled.
When Not to Use useMemo
For cheap calculations. A string concatenation, a simple arithmetic expression, a small array map — these take microseconds. useMemo adds more overhead than it saves.
// ❌ Over-engineered — this is not expensive
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
);
// ✅ Just compute it
const fullName = `${firstName} ${lastName}`;For primitive values. useMemo stabilizes object references. Primitive values (strings, numbers, booleans) already compare by value — no memoization needed.
As a first resort. If a component is slow because it renders too often, the first question is whether its state can be moved down or whether the parent can avoid passing unstable object props. useMemo is a targeted fix for when restructuring isn't practical.
The Right Mental Model
useMemo is a cache with one slot. It stores the last result alongside the last dependencies. On each render, it checks: "did the inputs change?" If not, it hands back the stored result. If yes, it recomputes and stores the new result.
Two specific problems it solves:
- Skip expensive work when inputs haven't changed
- Stabilize object/array references so downstream
memocomparisons work correctly
Everything else — the syntax, the dependency arrays, the caveats — flows from understanding those two jobs.
Practice what you just read.
Keep reading
Enjoyed this? Get more like it.
Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.