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:
- Expensive calculations
- Objects/arrays that are deeply compared
- Callback functions passed down to components that deeply rely on referential equality (
useEffectdependencies, etc.)
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:
- Unnecessary network requests
- Unbatched state updates
- Poor architecture (giant, monolithic components, deep prop drilling)
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.
- Store the function:
useRef(fn)keeps the function in a “box”(the current object), that persists through re-renders. - Always update:
ref.current = fnupdates the box with the latest version of the function each time. - Return a stable wrapper:
useCallbackcreates 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
- Don’t obsess over
useMemo,useCallback, orReact.memo. They are tools, not rules. - Focus on good component architecture: break up large components, avoid unnecessary renders by isolating heavy computations, and trust React’s efficiency.
- Optimize after profiling: Use React DevTools to find real bottlenecks, not imaginary ones.
- Memoize only when you need to. Most of the time, you don’t.
Keep things simple. Clean code is easier to maintain—and often ends up faster, too!