Skip to content
React rc performance 4 min read

useMemo & useCallback for Performance

useMemo and useCallback are React’s two caching hooks. They let you remember a computed value or a function identity between renders, so that expensive work is skipped and so that referentially stable props can flow into memoized children. Used well, they cut wasted work; used reflexively on everything, they add overhead and noise without measurable benefit. This page focuses on the cases where they genuinely move the performance needle.

How the two hooks differ

Both hooks take a dependency array and recompute only when one of those dependencies changes. The difference is what they cache.

HookCachesReturnsTypical use
useMemo(fn, deps)The result of calling fn()A valueExpensive calculations, stable object/array props
useCallback(fn, deps)The function itselfA functionStable callback props passed to memoized children

In fact useCallback(fn, deps) is exactly equivalent to useMemo(() => fn, deps) — it is a convenience wrapper for the common case of memoizing a function reference.

Stabilizing references for React.memo children

The single most valuable use of these hooks is keeping props referentially stable so that a React.memo-wrapped child can skip re-rendering. React.memo performs a shallow comparison of props; a freshly created object, array, or function fails that comparison every render because its identity changes even when its contents do not.

Consider a child that is expensive to render and only depends on a callback:

import { memo, useState, useCallback } from 'react';

const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
  console.log('ExpensiveList rendered');
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={() => onSelect(item.id)}>
          {item.label}
        </li>
      ))}
    </ul>
  );
});

function Dashboard({ items }) {
  const [count, setCount] = useState(0);

  // Stable identity across renders that don't change `items`
  const handleSelect = useCallback((id) => {
    console.log('selected', id);
  }, []);

  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>Clicked {count}</button>
      <ExpensiveList items={items} onSelect={handleSelect} />
    </>
  );
}

When you click the button, Dashboard re-renders, but handleSelect keeps the same identity and items is unchanged, so ExpensiveList is skipped.

Output:

// Clicking the button repeatedly logs nothing from the child.
// "ExpensiveList rendered" prints only on the initial mount.

Remove the useCallback and pass an inline onSelect={(id) => ...} instead, and the child re-renders on every click because the function prop is a brand-new value each time — defeating the memo entirely.

Memoizing a callback only helps if the consumer is itself memoized. Wrapping a handler in useCallback and then passing it to a plain, non-memoized child buys you nothing.

Memoizing expensive computations

The second core use is caching the result of a genuinely costly calculation so it does not rerun on unrelated renders.

import { useMemo, useState } from 'react';

function ProductTable({ products }) {
  const [query, setQuery] = useState('');

  const sorted = useMemo(() => {
    console.log('sorting products');
    return [...products].sort((a, b) => b.score - a.score);
  }, [products]);

  const visible = useMemo(
    () => sorted.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())),
    [sorted, query]
  );

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <p>{visible.length} of {products.length} products</p>
    </>
  );
}

Sorting runs only when products changes — not on every keystroke. Filtering recomputes when either the sorted list or the query changes. The useMemo also gives sorted a stable identity, which is useful if it were forwarded to a memoized child.

“Expensive” means measurably so. Sorting a handful of items or doing simple arithmetic is far cheaper than the bookkeeping useMemo adds. Profile before assuming a calculation is worth caching.

Dependency correctness

A memo hook is only as correct as its dependency array. Every reactive value used inside the function — props, state, and other memoized values — must appear in deps. Omitting one means your cache serves a stale value; adding extras means it busts too often.

// Wrong: `multiplier` is read but missing from deps -> stale result
const total = useMemo(() => price * multiplier, [price]);

// Correct
const total = useMemo(() => price * multiplier, [price, multiplier]);

Let the eslint-plugin-react-hooks exhaustive-deps rule manage this for you rather than reasoning by hand. If a dependency changes too often, the fix is usually to stabilize that dependency at its source (e.g. wrap it in its own useCallback/useMemo, or move it out of the component), not to silence the lint rule.

Do not over-memoize

These hooks are not free. Each call allocates an array, stores a closure, and runs a comparison on every render. Sprinkling them everywhere makes code harder to read and can be a net negative for performance. Reach for them when:

  • A value or callback is passed to a React.memo child or used in another hook’s dependency array.
  • A computation is provably expensive (measured with the Profiler).

For everything else, plain inline values are fine. Note also that the upcoming React Compiler automatically memoizes components and values at build time, which will make most manual useMemo/useCallback calls unnecessary in projects that adopt it.

Best practices

  • Memoize a callback or value only when its stable identity is actually consumed — by a memoized child or another hook’s dependencies.
  • Pair useCallback/useMemo props with a React.memo child; otherwise the memoization is wasted effort.
  • Keep dependency arrays exhaustive and lean on exhaustive-deps instead of trimming them manually.
  • Reserve useMemo for computations you have measured as expensive, not trivial maps or arithmetic.
  • Fix unstable dependencies at the source rather than memoizing around the symptom.
  • Profile before and after with React DevTools to confirm a memo actually removes renders.
  • Plan for the React Compiler — write clean components now and let manual memoization fade as you adopt it.
Last updated June 14, 2026
Was this helpful?