ESC

Type to search the knowledge base.

useMemo and useCallback

When memoization helps — referential equality, expensive pure work, memo children, dependency arrays, and the cost of caching everything.

intermediate6 min read
  • react
  • usememo
  • usecallback
  • performance
  • memo

useMemo and useCallback cache a value or a function identity between renders. They do not make React “faster” by default. They fix specific problems: expensive pure calculations you do not want to redo, and unstable props that break memo / dependency arrays.

If you wrap every expression in useMemo, you pay hook overhead, dependency bookkeeping, and cognitive load — often for zero user-visible win. Measure, then memoize the edges that matter.

Reconcile first: Reconciliation. Hooks order still applies: Rules of Hooks.

The problem they solve

Every render of a function component runs the body again. That creates new object and function identities even when contents are logically the same:

function Parent({ items }) {
  const [q, setQ] = useState('');
  const filters = { q }; // new object every render
  const onSelect = (id) => console.log(id); // new function every render

  return <ItemList items={items} filters={filters} onSelect={onSelect} />;
}

If ItemList is wrapped in React.memo, it still re-renders whenever Parent does — filters and onSelect failed shallow compare. useMemo / useCallback stabilize those references when dependencies haven’t changed.

Model

const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b]);
const memoizedFn = useCallback((...args) => doThing(a, ...args), [a]);
Hook Caches Recomputes when
useMemo(() => value, deps) Return value of the factory Any dep changes (Object.is)
useCallback(fn, deps) The function itself Any dep changes

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps) for function identity. Prefer useCallback for readability when the product is a callback.

Important: the factory for useMemo should be pure. Do not fetch, subscribe, or write refs for side effects inside it — use useEffect / event handlers.

Example: expensive pure derivation

function ProductGrid({ products, sortKey, hideOutOfStock }) {
  const visible = useMemo(() => {
    let list = products;
    if (hideOutOfStock) list = list.filter((p) => p.stock > 0);
    return [...list].sort((a, b) => compare(a, b, sortKey));
  }, [products, sortKey, hideOutOfStock]);

  return visible.map((p) => <Card key={p.id} product={p} />);
}

Worth it when products is large and the parent re-renders often for unrelated state (theme toggle, open dropdown). Not worth it for ten items and a cheap filter.

If the parent only re-renders when products / filters change anyway, useMemo adds nothing.

Example: stable callback for memo child

const Row = memo(function Row({ item, onToggle }) {
  // expensive row chrome…
  return (
    <button type="button" onClick={() => onToggle(item.id)}>
      {item.label}
    </button>
  );
});

function List({ items }) {
  const [selected, setSelected] = useState(() => new Set());

  const onToggle = useCallback((id) => {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }, []); // setSelected is stable

  return items.map((item) => (
    <Row key={item.id} item={item} onToggle={onToggle} />
  ));
}

Without useCallback, every parent render gives every Row a new onToggle → all memoized rows re-render. With a stable callback, only rows whose item prop changed re-render (assuming item identities are stable).

List identity still needs correct keys.

Example: dependency arrays and effects

function Search({ query }) {
  const params = useMemo(() => ({ q: query, limit: 20 }), [query]);

  useEffect(() => {
    fetch(`/api?${new URLSearchParams(params)}`)
      .then(/* … */);
  }, [params]);
}

Here useMemo keeps params referentially stable when query is unchanged so the effect does not re-fire spuriously. Alternative: depend on query directly and build params inside the effect — often clearer:

useEffect(() => {
  const params = { q: query, limit: 20 };
  // fetch…
}, [query]);

Do not invent memo layers to silence exhaustive-deps without understanding the data flow.

What useMemo does not do

  • Does not skip the component render by itself — the component still ran to call useMemo.
  • Does not deep-compare props for children — that’s memo + stable props.
  • Does not replace state — cached values are not reactive sources.
  • Does not guarantee cache hits across remounts — remount resets hooks.
  • Is not free — stores deps, compares them, retains the value (memory).

React docs increasingly frame memoization as a tool for semantic stability and measured performance, not a default coding style.

Composition: memo + useCallback is a system

Parent state update
  → Parent body runs
  → Child receives props
  → If Child is memoized AND every prop is Object.is-equal to last time
      → Child render skipped

Unstable props break the chain at the last step. Fix one of:

  1. Stabilize the prop (useMemo / useCallback / hoist constants)
  2. Move state down so Parent doesn’t re-render
  3. Pass children / composition so the expensive subtree isn’t recreated by the parent’s other state
  4. Accept the re-render if it’s cheap

Composition often beats blanket memoization — pass children that already rendered higher up.

Anti-patterns

// Useless: primitive already compared by value
const n = useMemo(() => count + 1, [count]);

// Harmful noise: tiny object, child not memoized
const style = useMemo(() => ({ color: 'red' }), []);

// Broken: missing deps — stale closure
const onClick = useCallback(() => {
  console.log(value);
}, []); // value frozen forever

Always list every reactive value from component scope that the callback/factory reads — same discipline as useEffect.

Compiler note (mental model for 2025+)

React’s compiler can auto-memoize in some setups. Until your repo relies on it, hand-written useMemo/useCallback remain the portable control surface. Don’t delete measured memoization because “the compiler will save us” unless the compiler is actually enabled and verified in your pipeline.

Footguns

  1. Memoizing without a memoized consumer or expensive cost — pure ceremony.
  2. Unstable deps (useMemo(() => f(obj), [obj]) where obj is new every time).
  3. Putting non-pure work in useMemo.
  4. Over-stabilizing so children never update when you forgot a dep.
  5. Using useCallback to “optimize” a button that isn’t heavy — noise in review.
  6. New array method results as deps — items.filter(...) inline in the dep array creates a new array every render; memoize that result or depend on items and filter inside.

Interview angle

Prompt: “Difference between useMemo and useCallback? When do you use them?”

Strong answer: “useMemo caches a computed value; useCallback caches a function reference. Both recompute when dependencies change. I use them when referential equality matters — memoized children, effect deps, or expensive pure derivations — after identifying a real cost. They don’t stop the parent from rendering; they make certain values stable across renders.”

Follow-up demo: show a memo child breaking on inline onClick={() => …} and fixed with useCallback.

Further reading

Related guides