DEV Community

Cover image for Why (and When) You Should Actually Use useMemo and useCallback
Hamed Farazi
Hamed Farazi

Posted on

Why (and When) You Should Actually Use useMemo and useCallback

If you've spent any time in a React codebase, you've probably seen useMemo and useCallback sprinkled everywhere — sometimes on every single function and computed value, "just in case." The irony is that this habit often makes an app slower, not faster, while also making the code harder to read.

This article is about building an honest mental model for these two hooks: what they actually do under the hood, when they genuinely help, and when they're just noise.

What these hooks actually do

useMemo and useCallback don't make anything "faster" by default. They cache a value between renders and only recompute it when its dependencies change.

const value = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const fn = useCallback(() => doSomething(a, b), [a, b]);
Enter fullscreen mode Exit fullscreen mode

useCallback(fn, deps) is really just useMemo(() => fn, deps). Same mechanism, different return type (a function reference instead of a value).

Two things matter here:

  1. The caching itself has a cost. React has to store the previous dependency array, compare it on every render, and hold onto the cached value in memory.
  2. You only benefit if something downstream actually cares about referential stability or expensive recomputation.

If neither of those is true, you're paying the comparison cost for zero benefit.

The case where it's obviously worth it: expensive computations

This one is uncontroversial. If you have a genuinely heavy calculation — sorting a large array, filtering thousands of rows, running some numeric computation — recomputing it on every render (including renders triggered by unrelated state) is wasteful.

function ProductList({ products, filters }) {
  const filtered = useMemo(
    () => products.filter(p => matchesFilters(p, filters)),
    [products, filters]
  );

  return <List items={filtered} />;
}
Enter fullscreen mode Exit fullscreen mode

Without useMemo, every re-render of ProductList — even one triggered by, say, a tooltip's hover state — would re-run the filter over the whole array. With it, the filter only reruns when products or filters actually change.

The key word is expensive. Filtering 20 items is not expensive. Filtering 20,000 items, or running a computation with real algorithmic weight, is. Don't reach for useMemo reflexively — profile first, or at least reason about the actual cost.

The case that trips people up: referential stability

This is where most of the confusion lives. useCallback and useMemo matter for referential stability in two situations:

1. When a value or function is a dependency of another hook.

function SearchBox({ onSearch }) {
  const debouncedSearch = useMemo(
    () => debounce(onSearch, 300),
    [onSearch] // if onSearch changes identity every render, this debounce is recreated every time
  );
  // ...
}
Enter fullscreen mode Exit fullscreen mode

If onSearch isn't stable, useEffect, useMemo, or a custom hook relying on it as a dependency will re-run constantly, potentially defeating the entire purpose of the debounce, subscription, or cache it's feeding into.

2. When a value is passed to a memoized child component.

const ExpensiveChild = React.memo(function ExpensiveChild({ onClick }) {
  // heavy render logic
});

function Parent() {
  const [count, setCount] = useState(0);

  // Without useCallback, this is a NEW function every render,
  // which defeats React.memo on ExpensiveChild entirely.
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <ExpensiveChild onClick={handleClick} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, React.memo on ExpensiveChild only pays off if the props it receives are stable. If handleClick is a new reference on every render, React.memo does nothing — the child re-renders anyway, and you've added the overhead of React.memo's comparison for no benefit.

This is the important part: useCallback without React.memo (or a dependency array somewhere downstream) usually does nothing useful. The two are a pair. If nothing consumes the stable reference, you're just adding bookkeeping.

When it's not worth it

  • Simple values or inline functions passed to plain DOM elements. <button onClick={() => setOpen(true)}> does not need useCallback. DOM elements aren't memoized components; a new function reference every render costs essentially nothing.
  • Cheap computations. const total = a + b doesn't need useMemo. The comparison overhead of the hook can genuinely exceed the cost of just recalculating.
  • "Just in case" usage without a memoized consumer. If nothing downstream is React.memo-wrapped and nothing uses the value as a dependency, the memoization has no one to benefit from it.
  • Values that change on every render anyway. If a dependency changes every render, useMemo will recompute every time too — you get the comparison cost and the recomputation.

A practical mental checklist

Before adding useMemo or useCallback, ask:

  1. Is this computation actually expensive, or does it just look intimidating? (Measure if unsure.)
  2. Does something downstream depend on referential equality — a React.memo child, a useEffect dependency, a custom hook's cache key?
  3. Do the dependencies change rarely relative to how often the component re-renders? If they change every time anyway, memoization buys nothing.

If the answer to #1 or #2 is no, you probably don't need the hook.

A note on the React Compiler

It's worth mentioning: the React team has been building an automatic compiler (React Compiler, formerly "React Forget") specifically to memoize things like this automatically, precisely because manual useMemo/useCallback placement is easy to get wrong and tedious to maintain. That's a strong signal about how the React team itself views manual memoization — as a mechanism you should reach for deliberately, not a reflex.

Takeaway

useMemo and useCallback are not performance switches you flip on by default. They're targeted tools for two specific problems: avoiding expensive recomputation, and preserving referential stability for something that actually checks for it. Used with intent, they meaningfully help. Used everywhere, they add cognitive overhead and runtime cost while often fixing nothing at all.


Where to get cover images for a post like this: for genuinely free-to-use, no-attribution-needed photos, Unsplash and Pexels are the standard choices for dev.to-style covers — search something like "code" or "abstract programming." For a more on-topic illustration (e.g. a diagram-style cover), you can also generate a simple custom SVG/graphic yourself — dev.to covers are just images, so anything royalty-free works.

Top comments (0)