useMemo: Memoizing Expensive Calculations

useMemo does two different jobs — and knowing which one you need makes the difference between a useful optimization and noise.

March 22, 20264 min read3 / 5

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

TSX
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.

TSX
// 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.

TSX
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.

TSX
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:

TSX
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:

TSX
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.

TSX
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.

TSX
// ❌ 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:

  1. Skip expensive work when inputs haven't changed
  2. Stabilize object/array references so downstream memo comparisons work correctly

Everything else — the syntax, the dependency arrays, the caveats — flows from understanding those two jobs.

Practice what you just read.

Skip Expensive Work with useMemo
1 exercise

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.