Skip to main content

How useMemo works and why is it needed

useMemo is a React hook that caches a computed value between re-renders, recalculating it only when specified dependencies change.

Theory

TL;DR

  • Think of a chef who preps an expensive sauce once per shift and reuses it unless key ingredients change.
  • Without useMemo, every render runs the calculation again, even if inputs are identical.
  • React stores the result and compares deps with Object.is. If nothing changed, it returns the cached value.
  • Use it when compute takes more than ~5ms and deps are stable across most renders.
  • Functions go to useCallback, values go to useMemo.

Quick example

jsx
import { useMemo, useState } from 'react'; const TodoList = ({ todos }) => { const [filter, setFilter] = useState('all'); // Without useMemo: filters on EVERY render, even if filter didn't change // const visible = todos.filter(t => t.status === filter); const visible = useMemo( () => todos.filter(t => t.status === filter), [todos, filter] // only recompute when these change ); return <ul>{visible.map(t => <li key={t.id}>{t.text}</li>)}</ul>; };

On a parent re-render where filter and todos haven't changed, useMemo returns the cached array. No filtering happens.

Key difference

Without useMemo, expensive functions run on every render regardless of whether inputs changed. That alone is a CPU cost. But there's a second problem: every render produces a new object or array reference, which breaks referential equality for child components and triggers their re-renders too. useMemo stores the previous result in the component's fiber node, compares each dep with Object.is, and skips the callback when nothing changed. One call saves both the compute and the downstream re-renders.

When to use

  • Heavy filter, sort, or reduce on large arrays (1,000+ items) - useMemo.
  • Derived state that stays stable across most renders - useMemo.
  • Object or array passed as a prop to a React.memo component - useMemo.
  • Computation under 1ms - just inline the expression.
  • Deps that change on every render - skip it, the cache will never hit.

How it works internally

React stores each useMemo result in a memoizedState linked list on the component's fiber node. On first render it runs mountMemo and saves both the value and the deps array. On re-render it runs updateMemo: it loops through the deps with Object.is (this is areHookInputsEqual in the React source), and if every dep matches, returns the cached value without calling the callback. If any dep changed, it calls the callback, stores the new result, and continues.

Two things worth knowing. First, useMemo runs synchronously during the render phase, so async callbacks don't work inside it. Second, the cache is not permanent. React can discard it under memory pressure or when the fiber is thrown away on unmount. It's an optimization hint, not guaranteed storage.

useMemo vs useCallback

useMemouseCallback
ReturnsA valueA function
Use caseComputed data, objects, arraysEvent handlers, callbacks
ExampleuseMemo(() => a + b, [a, b])useCallback(() => doX(), [x])
When to skipCheap computeCallback never passed to children

Common mistakes

Unstable object in deps

jsx
// Recomputes every render - { key: 'value' } is a new ref each time const value = useMemo(() => heavyCalc(items), [items, { key: 'value' }]); // Fix: use the primitive directly const value = useMemo(() => heavyCalc(items), [items, config.key]);

Memoizing cheap operations

jsx
// Hook overhead is bigger than the savings here const doubled = useMemo(() => a * 2, [a]); // Just inline it const doubled = a * 2;

Mutating the memoized value

jsx
const list = useMemo(() => items.filter(isActive), [items]); list.push(newItem); // Stales the cache - React assumes the callback is pure // Fix: create a new array const withNew = [...list, newItem];

Missing deps

jsx
// userId changes but useMemo never recomputes because deps is [] const result = useMemo(() => fetchData(userId), []); // Fix: add userId to deps const result = useMemo(() => fetchData(userId), [userId]);

Nested useMemo cascade

jsx
const data1 = useMemo(() => heavy1(input), [input]); const data2 = useMemo(() => heavy2(data1), [data1]); // data1 is new -> data2 always recomputes // Fix: combine into one memo for the whole pipeline const { data1, data2 } = useMemo(() => { const d1 = heavy1(input); return { data1: d1, data2: heavy2(d1) }; }, [input]);

In production I've seen this cascade pattern tank performance in dashboards that chain 4-5 derived states. Combining them into one useMemo cut re-render time by 60% in those cases.

Real-world usage

  • TanStack Query memoizes query results so stable data never triggers downstream renders.
  • Redux Toolkit's createSelector (from Reselect) implements the same idea as useMemo but at the store level.
  • Next.js uses memoized derived metadata during getStaticProps processing.
  • React.memo + useMemo is the standard combo: React.memo skips the child component re-render, useMemo keeps the props referentially stable so the skip actually fires.

Follow-up questions

Q: What is the time complexity of dep comparison in useMemo?
A: O(n) where n is the number of deps. React loops through each one with Object.is. Keep the deps array short, ideally under 5 entries.

Q: Does useMemo block rendering?
A: Yes. The callback runs synchronously during the render phase. If the computation is slow enough to block the UI, useMemo alone won't fix it - you need to move the work off the main thread.

Q: What's the difference between useMemo and React.memo?
A: useMemo caches a value inside a single component. React.memo wraps a component and skips its re-render if props haven't changed. They solve different problems but work well together.

Q: What happens if a dep is a Date object?
A: Object.is compares by reference. A new Date() call every render creates a new instance, so the comparison always fails and useMemo always recomputes. Use a primitive like a timestamp number or ISO string as the dep instead.

Q: (Senior) How does concurrent mode affect the useMemo cache?
A: In React 18, low-priority work can be interrupted and fibers can be discarded. When a fiber is dropped, its memoizedState is lost. If the work restarts, useMemo recomputes from scratch. For non-urgent memos, wrap the state update in startTransition so React knows it can deprioritize the work without causing stale data issues.

Examples

Basic: filtering a product list

jsx
import { useMemo, useState } from 'react'; const ProductList = ({ products }) => { const [search, setSearch] = useState(''); const filtered = useMemo( () => products.filter(p => p.name.toLowerCase().includes(search.toLowerCase())), [products, search] ); return ( <> <input value={search} onChange={e => setSearch(e.target.value)} /> <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul> </> ); };

filtered only recalculates when products or search changes. Typing in the input triggers a recalculate. A parent re-render with the same props does not.

Intermediate: computing stats in a todo app

jsx
const TodoStats = ({ todos }) => { const { activeCount, completedCount } = useMemo(() => { let active = 0; let completed = 0; todos.forEach(todo => { if (todo.completed) completed++; else active++; }); return { activeCount: active, completedCount: completed }; }, [todos]); return <p>{activeCount} active, {completedCount} completed</p>; };

The object returned from useMemo is the same reference across re-renders as long as todos doesn't change. That matters when TodoStats is wrapped in React.memo, because a new object reference every render would break the skip.

Advanced: unstable object deps

jsx
// This breaks - config is a new object on every parent render const BadChart = ({ data, config }) => { const processed = useMemo( () => data.map(item => ({ ...item, color: config.theme })), [data, config] // config !== prev config even if theme didn't change ); return <Chart data={processed} />; }; // Fix - destructure the primitive you actually care about const GoodChart = ({ data, config }) => { const { theme } = config; const processed = useMemo( () => data.map(item => ({ ...item, color: theme })), [data, theme] // primitive comparison works correctly ); return <Chart data={processed} />; };

This edge case trips a lot of experienced devs. The linter won't always catch it because config is technically used inside the callback. The fix is to destructure at the top and put only the primitives in the deps array.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?