ReactPerformanceMedium

How does useMemo work in React?

01

The Short Answer

useMemo caches the result of an expensive computation between renders. It only recalculates when its dependencies change. This prevents redundant work — if the inputs haven't changed, React returns the previously computed value instead of running the calculation again. It's the value equivalent of useCallback (which memoizes functions).

02

The Problem It Solves

Every time a component re-renders, all code inside the function body runs again — including expensive computations. If you're filtering a 10,000-item list or doing complex math on every render, that work repeats even when the inputs haven't changed. useMemo lets you skip the recalculation when the result would be the same.

In the example below, filteredProducts is recalculated on every keystroke in the search input — even when the products array and filter criteria haven't changed. If the product list is large, this causes noticeable lag.

without-memo.tsxtypescript
function ProductPage() {
  const [searchQuery, setSearchQuery] = useState('');
  const [sortBy, setSortBy] = useState('name');
  const [products] = useState(generateLargeProductList()); // 10,000 items

  // ❌ Runs on EVERY render — even when only searchQuery changed
  // Sorting 10,000 items is expensive and unnecessary if products/sortBy didn't change
  const sortedProducts = [...products].sort((a, b) => {
    if (sortBy === 'name') return a.name.localeCompare(b.name);
    return a.price - b.price;
  });

  // This filter is cheap, but it depends on the expensive sort above
  const filteredProducts = sortedProducts.filter(p =>
    p.name.toLowerCase().includes(searchQuery.toLowerCase())
  );

  return (
    <div>
      <input value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
      <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
        <option value="name">Name</option>
        <option value="price">Price</option>
      </select>
      <ProductList products={filteredProducts} />
    </div>
  );
}
03

The Solution: useMemo

By wrapping the expensive sort in useMemo, it only recalculates when products or sortBy actually change. Typing in the search input no longer triggers the sort — React returns the cached sorted array and only the cheap filter runs.

with-memo.tsxtypescript
function ProductPage() {
  const [searchQuery, setSearchQuery] = useState('');
  const [sortBy, setSortBy] = useState('name');
  const [products] = useState(generateLargeProductList());

  // ✅ Only recalculates when products or sortBy changes
  const sortedProducts = useMemo(() => {
    console.log('Sorting...'); // Only logs when products/sortBy change
    return [...products].sort((a, b) => {
      if (sortBy === 'name') return a.name.localeCompare(b.name);
      return a.price - b.price;
    });
  }, [products, sortBy]);

  // Filter is cheap — no need to memoize (but you could if needed)
  const filteredProducts = sortedProducts.filter(p =>
    p.name.toLowerCase().includes(searchQuery.toLowerCase())
  );

  return (
    <div>
      <input value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
      <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
        <option value="name">Name</option>
        <option value="price">Price</option>
      </select>
      <ProductList products={filteredProducts} />
    </div>
  );
}

Now typing in the search input is instant — the expensive sort is skipped because its dependencies (products, sortBy) haven't changed. The sort only re-runs when the user changes the sort dropdown.

04

The Second Use Case: Stable References

useMemo also prevents creating new object/array references on every render. This matters when you pass computed objects as props to memoized children or as dependencies to other hooks. Without useMemo, a new reference is created every render — defeating React.memo or causing useEffect to re-run unnecessarily.

stable-references.tsxtypescript
function Dashboard({ userId }: { userId: string }) {
  const [refreshCount, setRefreshCount] = useState(0);

  // ❌ New object every render — if passed to a memo'd child, it re-renders
  const config = { userId, theme: 'dark', locale: 'en' };

  // ✅ Same reference unless userId changes
  const config = useMemo(
    () => ({ userId, theme: 'dark', locale: 'en' }),
    [userId]
  );

  // ✅ Stable array reference for useEffect dependency
  const endpoints = useMemo(
    () => [`/api/users/${userId}`, `/api/users/${userId}/stats`],
    [userId]
  );

  useEffect(() => {
    // Without useMemo on endpoints, this runs every render
    endpoints.forEach(url => prefetch(url));
  }, [endpoints]); // Stable reference — only re-runs when userId changes

  return <SettingsPanel config={config} />;
}
05

useMemo vs useCallback

useMemo and useCallback are closely related — they both memoize something between renders. The difference is what they memoize: useMemo memoizes a computed value (the result of calling the function), while useCallback memoizes the function itself (without calling it).

memo-vs-callback.tstypescript
// useMemo — caches the RESULT of the function
const sortedList = useMemo(() => {
  return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// sortedList is the sorted array

// useCallback — caches the FUNCTION itself
const handleSort = useCallback(() => {
  return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// handleSort is a function you can call later

// They're equivalent:
useCallback(fn, deps)  ===  useMemo(() => fn, deps)
AspectuseMemouseCallback
MemoizesA computed valueA function reference
ReturnsThe result of calling the factoryThe function itself
Use forExpensive calculations, stable object referencesStable callback props for memo'd children
EquivalentuseMemo(() => value, deps)useMemo(() => fn, deps)
06

When NOT to Use useMemo

Memoization has overhead — React must store the previous value and compare dependencies on every render. For cheap computations, this overhead can exceed the cost of just recalculating. Don't memoize everything by default.

Skip useMemo when

  • The computation is trivial — simple math, string concatenation, or filtering a small array
  • The value isn't passed to a memoized child or used as a hook dependency
  • Dependencies change on every render — useMemo recalculates anyway, adding overhead for nothing
  • You haven't measured a performance problem — premature optimization adds complexity

Use useMemo when

  • The computation is genuinely expensive (sorting/filtering large arrays, complex math)
  • You need a stable object/array reference for React.memo children
  • You need a stable reference for useEffect/useCallback dependencies
  • You've profiled and confirmed the computation causes jank
07

Important Caveats

useMemo is a hint, not a guarantee

React may discard cached values in the future (e.g., for offscreen components or during memory pressure). Don't rely on useMemo for correctness — your code must still work if the value is recalculated. Use it only for performance optimization, never for preventing side effects.

08

Why Interviewers Ask This

This question tests whether you understand React's rendering performance model and can apply memoization judiciously. Interviewers want to see that you know what triggers recalculation, understand the difference between useMemo and useCallback, can identify when memoization helps vs when it's premature optimization, and understand the reference equality implications for child components and other hooks. It shows you think about performance with precision rather than cargo-culting optimization patterns.

Quick Revision Cheat Sheet

Purpose: Cache expensive computation results between renders

Recalculates when: Any dependency changes (Object.is comparison)

Second use: Stable object/array references for memo'd children or hook deps

vs useCallback: useMemo caches a value, useCallback caches a function

Don't overuse: Memoization has overhead — only use for genuinely expensive work

Not a guarantee: React may discard cached values — don't rely on it for correctness