DEV Community

Nainik Mehta
Nainik Mehta

Posted on

Stop Overusing useMemo & useCallback in React (2026)

Stop Overusing useMemo and useCallback — Memoize After Design

Hot take: wrapping every function or object in useCallback/useMemo usually makes your app slower and harder to maintain. The better approach is simple: profile first, move state down, and only target memoization to cases where it actually helps — typically React.memo-backed children or expensive computations.

Why the reflex to memoize is toxic

For a long time useMemo and useCallback felt like safe armor: a changed prop? Wrap it. A recreated function? useCallback it. In practice this pattern creates new problems:

  • CPU & memory tax: every useMemo/useCallback call adds work — dependency-array comparisons and cached storage. In extreme cases you allocate thousands of closures per frame, triggering GC spikes that are slower than a cheap re-render.
  • Staleness & bugs: fragile dependency arrays are a common source of subtle, silent bugs. Forget one dependency and you capture stale state.
  • False confidence: memoizing everywhere hides design issues like state lifted too high, unstable props, or children that re-render for unrelated reasons.

React 19 and the React Compiler reduce the need for manual memoization by automatically stabilizing many values at build time. That doesn’t mean memoization is dead — it means your strategy should change: measure first, then optimize deliberately.

A concrete (real-world) mistake

Imagine a parent renders a list of 2,000 rows. Naively you might memoize an onClick per row:

// BAD: this creates a new memoized closure per row on every parent render
function BigList({items}) {
  return (
    <div>
      {items.map(item => (
        <Row
          key={item.id}
          data={item}
          onClick={useCallback(() => handleRowClick(item.id), [item.id])}
        />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What looks defensive is actually allocative: every render calls useCallback for each row and allocates thousands of closure objects. If a different prop (for example, an unstable object passed down) changes, React.memo on Row can't bail out anyway — the memoization did nothing but increase memory churn.

The fix: share handlers and stabilize identity

A much cheaper pattern is to create a single handler and pass the row id:

// GOOD: one shared handler, no per-row closure allocations
function BigList({items}) {
  const handleClick = useCallback((id) => {
    // single, stable function reference reused by all rows
    console.log('row clicked', id);
  }, []);

  return (
    <div>
      {items.map(item => (
        <Row
          key={item.id}
          data={item}
          onClick={() => handleClick(item.id)}
        />
      ))}
    </div>
  );
}

const Row = React.memo(function Row({data, onClick}) {
  // Row will skip renders if `data` and `onClick` identity are stable
  return <div onClick={onClick}>{data.title}</div>;
});
Enter fullscreen mode Exit fullscreen mode

Note: you still create a small inline arrow for each Row in the example, but the heavy cost (thousands of memoized closures) is avoided. If Row expects a stable function reference to skip renders entirely, pass a bound handler like onClick={handleClick.bind(null, item.id)} cautiously, or better yet, have Row receive the id and call a stable handler internally.

Rules I follow in production

1) Profile first

  • Always use React DevTools Profiler (and production builds) before adding memoization. Look at actualDuration vs baseDuration and the "why did this render" reasons. Don’t optimize what isn’t slow.

2) Move state down

  • If a child re-renders when unrelated parent state changes, consider moving that piece of state closer to the child or splitting the parent. Reducing render frequency is usually cheaper than memoizing everything.

3) Target memoization

  • Only memoize values or functions that are consumed by memoized children (React.memo) or that gate an expensive computation. useCallback/useMemo are tools to preserve referential stability — they only buy you anything when someone checks that identity.

Practical decision tree

  • Is the computation expensive (consistently > ~1ms under realistic load)? useMemo may help.
  • Is a function passed to a React.memo child (or used in a deep dependency)? useCallback may help.
  • Is the component re-rendering frequently with mostly stable props? Consider React.memo for the child and stabilize the props upstream.
  • Otherwise: leave it alone.

Three quick fixes you can apply right now

1) Replace per-item useCallback with a shared handler that receives an id.
2) Convert stable-value props to primitives or refs so identity is stable across renders (for example, move a static config object outside the component or memoize it once).
3) Wrap truly expensive children with React.memo, then only add useCallback/useMemo for props that break that memo boundary.

How React 19 and the Compiler change the calculus

React 19 and the React Compiler automate many of the memoization patterns developers used to write by hand. The compiler can insert precise memoization at build time without the dependency-array footguns. This means:

  • Less manual memoization for new code; plain code is often best.
  • Existing useMemo/useCallback calls become mostly redundant where the compiler covers the component.
  • You still need to profile, fix impurity bugs (mutations, side effects in render), and address architectural bottlenecks like data-fetch waterfalls and oversized DOM.

Don’t treat the compiler as a free pass to ignore profiling — it’s a tool that reduces mechanical work, not a substitute for good design.

Common gotchas to watch for

  • Missing dependencies in useMemo/useCallback: leads to stale closures and subtle bugs.
  • Memoizing cheap work: the dependency checks and storage can cost more than the computation.
  • Passing fresh objects/functions as props: React.memo does a shallow comparison — new references always look different.

Closing: memoize after design, not before

Memoization is a surgical optimization, not a blanket fix. Start with readable, correct code. Profile realistic production builds. Move state to reduce unnecessary renders. Then, target memoization where it measurably helps — most often when protecting React.memo boundaries or skipping expensive recalculations.

What’s one place in your app where memoization helped — or caused more harm than good? Share an example and what you learned.

Top comments (0)