ReactPublished 4/21/2025

Rendering and Performance: When to Reach for useMemo, useCallback, and React.memo

Understand what a React re-render actually is, then use three common performance-optimization mistakes to figure out when useMemo, useCallback, and React.memo actually earn their keep.

The Real Nature of a Re-render: Just a Function Call

We covered this in useState and useEffect's Dependency Array: every render is really just React calling your component function again. Two things follow from that, and they're the foundation for everything else in this post.

First, every variable declared inside the function body — objects, arrays, functions — gets recreated from scratch on every render, even when the contents are identical.

js
function Toolbar() {
  const options = { theme: 'dark' } // a brand new object every render
  const handleClick = () => console.log('clicked') // a brand new function every render
 
  return <Button options={options} onClick={handleClick} />
}

Second, whenever a component re-renders, React re-renders all of its children by default, regardless of whether their props actually changed. That's not a bug — it's the default behavior, because figuring out "did the props actually change" is itself a comparison that costs something, and React leaves that decision up to us.

React.memo, useMemo, and useCallback all exist to deal with the fallout from these two facts: unnecessary child re-renders, and unnecessary recomputation. But they attack the problem from different angles, so let's look at each one separately.

React.memo: Blocking Unnecessary Child Re-renders

Wrap a component in React.memo and React will, on every parent re-render, first compare the props it's about to pass in against the props from last time (a shallow comparison by default). If they match, React reuses the previous render output instead of actually calling the component function again.

js
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  console.log('ExpensiveList rendered')
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
})

When is it actually worth it? Usually two conditions need to hold at once: the component is genuinely expensive to render (a long list, heavy internal computation, a big chunk of DOM), and its parent re-renders often while the props it passes down mostly stay the same. If the component is lightweight, rendering it once probably costs less than the comparison React.memo does — wrapping it just adds overhead for nothing.

Here's a gotcha worth flagging: if the items passed to ExpensiveList is built with an array literal or a fresh .map() call inside the parent, React.memo is basically wasted effort. Every parent render produces a new reference for items, so the shallow comparison will always say "different."

js
function Parent({ rawData }) {
  const items = rawData.map((d) => ({ id: d.id, name: d.name })) // a new array every time
 
  return <ExpensiveList items={items} /> // React.memo can't do anything here
}

This is why React.memo rarely does its job in isolation — whether it works at all depends on whether the props it receives hold a stable reference. And keeping objects, arrays, and functions referentially stable is exactly what useMemo and useCallback are for.

useMemo: Keeping Both the Result and the Reference

useMemo skips recomputing something on this render if its dependency array hasn't changed, and just hands back whatever it computed last time.

js
function Parent({ rawData }) {
  const items = useMemo(() => rawData.map((d) => ({ id: d.id, name: d.name })), [rawData])
 
  return <ExpensiveList items={items} />
}

There are actually two separate problems being solved here, and most people only notice one of them. The first is saving the cost of the computation itself: if rawData hasn't changed, there's no need to run .map() again. This layer usually matters less than people think, because most computations are cheap enough that re-running them costs less than the dependency comparison useMemo does internally. The second layer is the one that actually matters: keeping items at the same reference when its dependencies haven't changed, so that when it's passed down to a React.memo-wrapped ExpensiveList, the shallow comparison correctly reads "unchanged" and actually blocks the re-render.

So deciding whether to reach for useMemo comes down to two questions: is this computation genuinely expensive (sorting a large array, some heavy data transformation)? Or does the referential stability of the result actually matter downstream — to a React.memo, a useEffect dependency array, or some other hook? If the answer to either is yes, useMemo earns its keep. If it's something as trivial as a + b, wrapping it in useMemo just adds complexity for no real benefit.

useCallback: Keeping a Function's Reference Stable

Conceptually, useCallback is the same idea as useMemo — it's just specialized for functions.

js
function Parent() {
  const [count, setCount] = useState(0)
 
  const handleClick = useCallback(() => {
    console.log('clicked')
  }, [])
 
  return <MemoizedButton onClick={handleClick} />
}

Without useCallback, handleClick would be a new function on every render. Even if MemoizedButton is wrapped in React.memo, it would still re-render every time, because onClick keeps changing reference — which defeats the whole point of React.memo. Pinning handleClick's reference with useCallback is what lets the memoization downstream actually work.

useCallback has another common use: keeping a function stable so it doesn't churn a dependency array on every render, which would otherwise cause an effect to keep re-firing.

js
function SearchBox({ onSearch }) {
  const search = useCallback(
    (keyword) => {
      onSearch(keyword.trim())
    },
    [onSearch],
  )
 
  useEffect(() => {
    const timer = setTimeout(() => search(inputValue), 300)
    return () => clearTimeout(timer)
  }, [search, inputValue])
}

Worth noting: useCallback doesn't make function creation disappear — it just defers the decision. What you're really buying is referential stability, not savings on computation (creating a function is essentially free). So the same rule applies as with useMemo: does this function's referential stability actually affect a React.memo or a dependency array somewhere downstream? If it's just an onClick handler passed straight to a DOM element, with no React.memo below it and no dependency array it feeds into, wrapping it in useCallback does nothing — it's pure overhead.

How the Three Fit Together — and Why Using Just One Often Falls Flat

By now the pattern should be clear: these three APIs form an interdependent toolkit, and using just one in isolation is rarely useful.

  • React.memo blocks re-renders, but only if the props it receives are referentially stable.
  • useMemo keeps object- and array-shaped props stable.
  • useCallback keeps function-shaped props stable.

Wrapping something in React.memo without fixing where its props come from is wasted effort. And using useMemo/useCallback with no React.memo downstream to take advantage of that stability means the stability you bought doesn't actually buy you anything (unless it's feeding into a dependency array). This is exactly why overusing these three APIs tends to bloat the code without delivering the performance win people expect — most of the time, there's no React.memo-wrapped component downstream actually waiting to cash in on that stability.

When Should You Actually Reach for These?

Rather than wrapping every component out of habit, it's more practical to reach for these tools only when one of the following actually applies:

The component is genuinely expensive to render — rendering a long list, drawing a chart, running heavy style calculations. That's when React.memo blocking unnecessary re-renders pays off visibly.

You've actually measured a performance problem. Use the React DevTools Profiler to confirm a component is re-rendering unusually often and that the rendering cost is actually causing jank, then optimize that specific bottleneck instead of optimizing on a hunch.

The computation itself is expensive — sorting or filtering a large array, running statistics. Here, the computation savings from useMemo are real, not just a side effect of referential stability.

A function or object feeds into a dependency array, and you need to stop useEffect, useMemo, or useCallback from re-firing every render because their own dependency array keeps changing. Pinning the reference with useCallback/useMemo here is solving an actual problem, not optimizing prematurely.

On the flip side, if a component is lightweight, its parent doesn't re-render often, or you haven't actually measured anything, useMemo, useCallback, and React.memo are mostly just reassuring the developer rather than the runtime — all they add is more code to read.

Wrapping Up

useMemo, useCallback, and React.memo all exist to solve the same underlying problem: avoiding unnecessary recomputation and unnecessary re-renders when a component updates. But they're not free, and they're not a silver bullet — each one has its own comparison and caching cost, and they usually only pay off when used together. Instead of reflexively wrapping every function component, profile first, find the actual bottleneck, and then reach for the tool that fixes that specific bottleneck. That's the difference between an optimization that actually works and one that's just decorative complexity.