AniUI Academy

useMemo and useCallback

Memoizing an expensive calculation and memoizing a function's identity across renders — what each buys you, and why reaching for either without measuring is often premature.

10 min read

React.memo skips a child's re-render when its props are unchanged — but that only works if the props reaching it are actually the same reference when nothing relevant changed. useMemo and useCallback are the two tools for making that true.

useMemo: memoizing a value

import { useMemo } from "react";
 
function ProductList({ products, filterText }) {
  const filtered = useMemo(() => {
    console.log("Filtering products...");
    return products.filter((p) => p.name.includes(filterText));
  }, [products, filterText]);
 
  return <ul>{filtered.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

Without useMemo, products.filter(...) would re-run on every render of ProductList — including renders triggered by something completely unrelated to products or filterText. useMemo re-runs the calculation only when one of the listed dependencies has actually changed since the last render, and returns the previously cached result otherwise. The dependency array works exactly like useEffect's — same comparison rules, same "list everything the calculation reads" discipline.

This also solves the reference-stability problem directly: filtered is now the same array reference across renders where products and filterText haven't changed, which matters if filtered is later passed as a prop to a React.memo-wrapped child.

useCallback: memoizing a function's identity

useCallback is the same idea, specialized for functions:

import { useCallback } from "react";
 
function ProductList({ products, onSelect }) {
  const handleSelect = useCallback(
    (id) => {
      onSelect(id);
      console.log("Selected", id);
    },
    [onSelect]
  );
 
  return <MemoizedRow products={products} onSelect={handleSelect} />;
}

Without useCallback, handleSelect would be a brand-new function on every render of ProductList — a new reference every time, even though its behavior never changes. If MemoizedRow is wrapped in React.memo, that fresh reference would defeat the memoization entirely (from the previous lesson's exact failure case), since the shallow prop comparison would see a "changed" onSelect every single render. useCallback(fn, deps) is really just useMemo(() => fn, deps) — it's not calling the function or caching its result, only caching the function reference itself.

The two exist to work together

The realistic pattern combines all three tools from this and the previous lesson:

const MemoizedRow = memo(function Row({ product, onSelect }) {
  return <li onClick={() => onSelect(product.id)}>{product.name}</li>;
});
 
function ProductList({ products, onSelect }) {
  const stableOnSelect = useCallback((id) => onSelect(id), [onSelect]);
 
  return (
    <ul>
      {products.map((p) => (
        <MemoizedRow key={p.id} product={p} onSelect={stableOnSelect} />
      ))}
    </ul>
  );
}

React.memo on Row only pays off because stableOnSelect keeps the same reference across renders where onSelect itself hasn't changed — without useCallback here, every row would still re-render on every list render, memo or not.

Both have a real cost — this is not a free win

useMemo and useCallback are not without cost: React still has to store the previous dependencies and compare them on every render, and for a genuinely cheap calculation (adding two numbers, formatting a short string) or a function with no memoized descendant depending on its stability, that bookkeeping is pure overhead with no skipped work to show for it. The practical guidance:

  • Reach for useMemo when a calculation is measurably expensive (looping over a large array, complex math) — not for trivial arithmetic.
  • Reach for useCallback specifically when the function is passed to a React.memo-wrapped component, or into another hook's dependency array where reference stability matters.
  • When in doubt, don't add either preemptively — a profiler showing an actual slow render is a better reason to memoize than a general instinct that "this might help."

Try it yourself

Try it yourself
Loading playground...

What to remember

  • useMemo caches a calculation's result, re-running it only when its dependencies change; useCallback caches a function's identity the same way.
  • Both solve the same underlying problem: giving a value or function a stable reference across renders, which is what lets React.memo (or another hook's dependency array) actually detect "nothing changed."
  • An inline function passed to a memoized component defeats the memoization, because it's a new reference every render — useCallback is the fix.
  • Both have a real per-render cost; reserve them for measurably expensive calculations or for stabilizing props into memoized children, not as a reflexive default.

Check yourself

4 questions · pass 3/4 to unlock Keys and List Reconciliation Pitfalls

up to 50
  1. 1.What does useMemo actually memoize?

  2. 2.What does useCallback memoize, as distinct from useMemo?

  3. 3.Why would passing an unmemoized inline function as a prop defeat a child wrapped in React.memo?

  4. 4.Why is 'wrap every calculation in useMemo and every function in useCallback' not automatically good practice?

4 left to answer