Marcell CD

Stop Overusing Memoization

Stop Overusing Memoization

If you’re like most React developers, chances are you’ve read advice about using useMemo, useCallback, and React.memo to squeeze more performance out of your apps. Maybe you’ve even dropped them into your component tree “just in case.” But I would say that most of the time these optimizations are unnecessary and might actually make your code harder to maintain, with little to no benefit at all.

Let’s take a closer look at why that’s the case, using some pragmatic code examples, and see what actually matters for real-world React performance.


Why Memoization Feels Like the Right Thing

Memoization is the process of caching the result of a function so you don’t have to recalculate it. In React, this is possible via hooks like useMemo (for expensive calculations) and useCallback (for event handlers or stable references).

At first glance, it seems obvious that if you memoize everything, nothing will ever needlessly re-render, and your app will be lightning fast. But there’s a catch.


Memoization Is Fragile

Consider a simple parent–child component tree. What happens if we try to “optimize” it with memoization?

function Parent() {
  // BAD: This 'list' prop is a new array every render!
  return <Child list={[1, 2, 3]} />
}

const Child = React.memo(({list}) => {
  console.log("Child rendered")
  return <Grandchild list={list} />
})

const Grandchild = React.memo(({list}) => {
  console.log("Grandchild rendered")
  return <div>{list.join(", ")}</div>
})

Problem:
Every time Parent renders, it creates a new array for list. This means Child always receives a new prop, so it always re-renders, no matter how much you wrap it in React.memo. Grandchild suffers the same fate—the memoization chain is broken at the top! And yes, we could simply define the list component outside the Parent component and make it static, in that way we would solve this specific issue, bu that is not the point. This becomes much harder working in large React codebases where it is super hard to track.


Don’t Over-Engineer, Most Renders Are Cheap

Here’s the best-kept secret: for 90% of components in React UIs, rendering is fast. Unless you’re performing computationally intensive tasks, such as sorting a large list or running regex on megabytes of data, React’s update-diff-paint pipeline is highly optimized. Rendering effectively updates the UI to the latest view, which is what we want to show the end user. We need to focus on optimizing the code when we encounter unnecessary or expensive re-renders.

So, do you need to memoize every callback or array prop? Definitely not! Doing so will make your code less readable and more fragile. It’s important to consider that memoizing values and functions also incurs a performance cost, as we need to store more items in memory.


When and How to Memoize

Do memoize:

Let’s fix our earlier example with useMemo:

function Parent() {
  const list = React.useMemo(() => [1, 2, 3], [])
  return <Child list={list} />
}

Or, since our list is just a hardcoded list of items that not need to be defined in out component we could define the list outside the Parent component.

const list = [1, 2, 3]
function Parent() {
  return <Child list={list} />
}

But—be honest—unless you’re seeing measurable performance hits in this part of your tree, you probably don’t need it!

Real performance bottlenecks most often come from:


One Modern Pattern: ”Latest Ref” for Stable Callbacks

Just a nice tip and a React pattern that I really like is the ”Latest Ref” for Stable Callbacks pattern, a pattern Kent C.Dodds writes about. If you really need a rock-solid, stable callback reference (but want to avoid dependency headaches), here’s a pro pattern from the best React minds:

function useLatest(fn) {
  const ref = React.useRef(fn)
  ref.current = fn
  return React.useCallback((...args) => ref.current(...args), [])
}

// Usage:
function MyComponent({onSomething}) {
  const stableHandler = useLatest(onSomething)
  React.useEffect(() => {
    // Use stableHandler here, no deps!
  }, [])
}

When you use a function inside useEffect, it can become “stale” (outdated) if the function changes.

  1. Store the function: useRef(fn) keeps the function in a “box”(the current object), that persists through re-renders.
  2. Always update: ref.current = fn updates the box with the latest version of the function each time.
  3. Return a stable wrapper: useCallback creates a function that remains constant while always calling whatever is currently in the box.

React Compiler

The React team is developing a compiler to automate much of this optimization, allowing you to write clean code while letting the robots handle the work behind the scenes. We can now start using the compiler, which helps us avoid the need to memorize details. However, not everyone can migrate to the compiler right away, as many teams are still using earlier versions of React. Until then, write small components, profile before optimizing, and use memoization sparingly and purposefully. Something to look forward to!


The Takeaway

Keep things simple. Clean code is easier to maintain—and often ends up faster, too!