Memoizing components in React: a case for useMemo

React.memo is often used to optimize component rendering, but useMemo offers more control and transparency. This article explains why useMemo could be a solution too to memoize components in React applications until the React Compiler take care of it for us.

As a React developer, you’ve likely encountered the problem of excessive renders: a component re-rendering without apparent reason, an interface that gradually slows down, or animations that stutter without obvious explanation. Faced with these symptoms, the solution that often comes to mind is to apply React.memo - the magical function that seems to promise to solve all our performance problems.

Yet, during my work on various projects, whether as a new developer joining a team or as a technical consultant, I consistently observe the same pattern: React.memo is present, sometimes even abundantly, but its usage is rarely effective. The reason? A misunderstanding of how it works, particularly regarding the memoization of props.

I don’t say that to blame anyone. React.memo is a powerful tool that can help, but it’s easy to miss out on a prop update as a codebase evolves.

Contents

A First Statement

As Donald Knuth said:

"

“Premature optimization is the root of all evil.”.

"

And it can’t be more true than here. To be honest, I very rarely had the need to use React.memo. I think I had to do it twice or thrice the past 5 years.

If you take time to analyze the root cause of your performance issues, you will probably find that the problem is elsewhere (debouncing, throttling, batching, etc.). So don’t rush too much to use it.

React.memo is more of a workaround than a solution. It can help, but it doesn’t solve the underlying problem. It’s like putting a band-aid on a wound.

What it looks like to me to have React.memo everywhere
What it looks like to me to have React.memo everywhere Photo by Luis Villasmil on Unsplash

But let’s go back to our problem. Why do we need to use React.memo in the first place?

The Problem: Unnecessary Re-renders

When a parent component re-renders, all of its children re-render by default - even if their props haven’t changed. That’s not a problem, React was designed like this and it’s fine to re-render your whole app on every state update (until it feels sluggish of course).

Let’s consider a simple example: a component that displays a list of food items. When the parent component updates its state (like a counter), the entire list and all its items re-render, even though the list data hasn’t changed.

The problem of unnecessary re-renders

Click the button and observe how all components re-render unnecessarily

App rendered: 1
FoodList rendered: 1
FoodItem-Apple rendered: 1
🍎Apple
FoodItem-Banana rendered: 1
🍌Banana
FoodItem-Pizza rendered: 1
🍕Pizza
If you click on the `Re-render App` button, all components will re-render, that's the default behavior in React
Interactive

In this example, clicking the “Re-render App” button causes:

  • the App component to re-render (expected)
  • the FoodList component to re-render (unnecessary)
  • all individual FoodItem components to re-render (unnecessary)

This cascading re-render pattern can significantly impact performance, especially with larger lists or more complex components. It may not have an impact at first, but it may as you add more components and your logic grows more complex.

Understanding Memoization in React

To avoid these useless re-renders, we often resort to memoization. Memoization is an optimization technique that stores the results of expensive function calls and returns the cached result for the same input. In React, this concept may be applied to prevent unnecessary re-renders.

React provides two primary APIs for memoization:

  1. React.memo: A higher-order component that memoizes an entire component
  2. useMemo: A hook that memoizes a value within a component

While both serve the purpose of optimization, they operate at different levels and with different characteristics.

Let’s compare how React.memo and useMemo solve it:

Using React.memo

React.memo is a higher-order component that wraps around your component:

const MemoizedComponent = React.memo(MyComponent);

This prevents the component from re-rendering unless its props change.

React.memo Solution

Click the button and observe how memoized components prevent re-renders

App rendered: 1
FoodList rendered: 1
FoodItem-Apple rendered: 1
🍎Apple
FoodItem-Banana rendered: 1
🍌Banana
FoodItem-Pizza rendered: 1
🍕Pizza
Interactive

Okay it seems simple enough.

Using useMemo

With useMemo, we can also memoize, this time not the component, but an element or an array of elements.

function App() {
const list = useMemo(() => {
return <FoodList items={foodList} />;
}, [foodList]);
// ...
}

It will have the same effect as React.memo. It relies on a different trick though: React won’t re-render element references that are the same. It’s very similar to composition.

useMemo Solution

Click the button and observe how useMemo prevents re-renders

App rendered: 1
FoodList rendered: 1
FoodItem-Apple rendered: 1
🍎Apple
FoodItem-Banana rendered: 1
🍌Banana
FoodItem-Pizza rendered: 1
🍕Pizza
Interactive

We have two solutions, now what?

The case of React.memo

I see multiple drawbacks with React.memo:

Global vs. Local Optimization

React.memo applies memoization at the component level, making it a “global” optimization for that component. This means:

  • the memoization affects all instances of the component
  • developers who use your component might not be aware it’s memoized
  • it’s easy to forget a component is memoized when revisiting code months later

Limited Dependency Control

React.memo only compares props by default (using shallow equality). While you can provide a custom comparison function, this approach is less explicit than useMemo’s dependency array:

const MemoizedComponent = React.memo(
MyComponent,
(prevProps, nextProps) => prevProps.id === nextProps.id,
);
function ParentComponent() {
const memoizedComponent = useMemo(
() => <MyComponent id={id} />,
[id], // Clear dependency list
);
return memoizedComponent;
}

Hidden Optimization

When a component is wrapped with React.memo, this optimization is not visible at the usage site:

function App() {
// Is ExpensiveComponent memoized? You can't tell from here
return <ExpensiveComponent data={data} />;
}

This lack of visibility can lead to confusion about performance characteristics and make debugging more difficult. A developer adds a prop to the component, and you are back with your performance problem.

useMemo as a better solution

Let’s look at why useMemo offers a better approach to memoization.

Explicit Dependency Tracking

useMemo requires you to specify dependencies in an array, making it clear what values should trigger recalculation:

const memoizedValue = useMemo(() => {
return <FoodList items={foodList} onSelect={handleSelect} />;
}, [foodList]); // Eslint will warn if you forget to add a dependency

This explicit dependency list:

  • helps catch bugs when dependencies are missed
  • provides clear signals to other developers about what triggers re-renders

Local and Visible Optimization

useMemo applies optimization locally, right where it’s needed:

function ParentComponent({ data }) {
const memoizedChild = useMemo(
() => <FoodList items={items} />,
[items], // Only re-render when items changes, not when name or data change
);
return <div>{memoizedChild}</div>;
}

This approach:

  • makes the optimization visible exactly where it’s applied
  • allows for selective memoization based on the specific usage context
  • improves code readability by making performance considerations explicit

Granular Control

useMemo allows for more granular control over what gets memoized:

function App() {
const child1 = useMemo(() => <Child1 />, [dependency1]);
const child2 = useMemo(() => <Child2 />, [dependency2]);
return (
<>
{child1}
{child2}
<Comments />
</>
);
}

This granularity allows you to optimize precisely where needed, rather than applying a blanket optimization to an entire component.

When React.memo Still Makes Sense

⚠️
Warning

While useMemo is generally preferable, there are still valid use cases for React.memo.

React.memo is appropriate when:

  1. You have a pure component that is used in many places with the same optimization needs
  2. You’re creating a reusable library component where the optimization should be built-in
  3. You need to optimize a third-party component you don’t control

However, even in these cases, consider whether a useMemo wrapper at the usage site might provide more clarity and control.

The Future: React Compiler

The React team is working on a compiler that will automatically apply memoization optimizations. It’s still in beta currently though. This compiler will analyze your code and insert memoization where beneficial, similar to how you would manually use useMemo.

For instance given the code below:

import React, { useState } from "react";
import { Box } from "../common/Box";
import { FoodList, FoodItem } from "../FoodComponents";
export function App({ foodItems }) {
const [count, setCount] = useState(0);
return (
<Box name="App">
<button onClick={() => setCount((c) => c + 1)} className="button-fancy">
Re-render App: {count}
</button>
<FoodList items={foodItems} />
</Box>
);
}

The React compiler results would look like this:

import { c as _c } from "react/compiler-runtime";
import React, { useState } from "react";
import { Box } from "../common/Box";
import { FoodList, FoodItem } from "../FoodComponents";
export function App(t0) {
const $ = _c(8);
const { foodItems } = t0;
const [count, setCount] = useState(0);
// ...
//
let t3;
if ($[3] !== foodItems) {
//
t3 = <FoodList items={foodItems} />;
$[3] = foodItems;
$[4] = t3;
} else {
//
t3 = $[4];
}
// ...
return t4;
}

What does it mean? Well, $ is our memoization cache. The compiler will check whether or not foodItems has changed (at index 3 of the cache):

  • if yes: it will re-render the component and update the cache by setting the element at position 4, and the new foodItems cache value at position 3.
  • if no: it will use the cached value at position 4.

You can have a look at the whole output in this playground.

💡
Info

This approach aligns more closely with the useMemo pattern than with memo.

This means that with the React Compiler, you don’t need to worry about memoizing components using useMemo or React.memo. The compiler will do it for you. The React Compiler identifies specific values and dependencies that should trigger re-renders for you, rather than broadly memoizing entire components.

When I give my code to the React Compiler and it fixes all my problems

Mary Poppins

Conclusion

While React.memo has its place in the React ecosystem, useMemo offers superior control, visibility, and maintainability in most scenarios:

  • it provides explicit dependency tracking
  • it makes optimization visible at the usage site
  • it allows for more granular control
  • it aligns with the direction of React’s future compiler optimizations

Next time you reach for React.memo, consider whether useMemo might offer a more transparent and maintainable approach to optimization in your React application or if switching to the React Compiler could be a better option.

And of course as I said earlier, don’t rush to use it. Take the time to analyze where re-renders are happening and if they are really a problem. You might find better approaches to solve your performance issues.

Feel free to share your opinion in the comments below.

References