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
- The Problem: Unnecessary Re-renders
- Understanding Memoization in React
- Using React.memo
- Using useMemo
- The case of React.memo
- useMemo as a better solution
- When React.memo Still Makes Sense
- The Future: React Compiler
- Conclusion
- References
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.

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
import React, { useState } from "react";import { Box } from "../common/Box";
function FoodItem({ emoji, name }: { emoji: string; name: string }) { return ( <Box name={`FoodItem-${name}`}> <div className="flex items-center space-x-2"> <span className="text-base">{emoji}</span> <span>{name}</span> </div> </Box> );}
function FoodList({ items,}: { items: Array<{ emoji: string; name: string }>; useMemoized?: boolean;}) { return ( <Box name="FoodList" className="space-y-2 mt-2" wrapperClassName="mt-2"> {items.map((item) => ( <FoodItem key={item.name} emoji={item.emoji} name={item.name} /> ))} </Box> );}
const foodItems = [ { emoji: "🍎", name: "Apple" }, { emoji: "🍌", name: "Banana" }, { emoji: "🍕", name: "Pizza" },];
export function App() { 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> );}
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:
- React.memo: A higher-order component that memoizes an entire component
- 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
import React, { useState } from "react";import { Box } from "../common/Box";
function FoodItem({ emoji, name }: { emoji: string; name: string }) { return ( <Box name={`FoodItem-${name}`}> <div className="flex items-center space-x-2"> <span className="text-base">{emoji}</span> <span>{name}</span> </div> </Box> );}
function FoodList({ items,}: { items: Array<{ emoji: string; name: string }>; useMemoized?: boolean;}) { return ( <Box name="FoodList" className="space-y-2 mt-2" wrapperClassName="mt-2"> {items.map((item) => ( <FoodItem key={item.name} emoji={item.emoji} name={item.name} /> ))} </Box> );}
const MemoizedFoodList = React.memo(FoodList);
const foodItems = [ { emoji: "🍎", name: "Apple" }, { emoji: "🍌", name: "Banana" }, { emoji: "🍕", name: "Pizza" },];
export function App() { const [count, setCount] = useState(0);
return ( <Box name="App"> <button onClick={() => setCount((c) => c + 1)} className="button-fancy"> Re-render App: {count} </button> <MemoizedFoodList items={foodItems} /> </Box> );}
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
import { useState, useMemo } from "react";import { Box } from "../common/Box";
function FoodItem({ emoji, name }: { emoji: string; name: string }) { return ( <Box name={`FoodItem-${name}`}> <div className="flex items-center space-x-2"> <span className="text-base">{emoji}</span> <span>{name}</span> </div> </Box> );}
function FoodList({ items,}: { items: Array<{ emoji: string; name: string }>;}) { return ( <Box name="FoodList" className="space-y-2 mt-2" wrapperClassName="mt-2"> {items.map((item) => ( <FoodItem key={item.name} emoji={item.emoji} name={item.name} /> ))} </Box> );}
const foodItems = [ { emoji: "🍎", name: "Apple" }, { emoji: "🍌", name: "Banana" }, { emoji: "🍕", name: "Pizza" },];export function App() { const [counter, setCounter] = useState(0);
const memoizedFoodList = useMemo(() => { return <FoodList items={foodItems} />; }, []);
return ( <Box name="App"> <div className="flex flex-col space-y-4"> <div className="flex space-x-2"> <button onClick={() => setCounter((c) => c + 1)} className="button-fancy" > Re-render App: {counter} </button> </div>
{memoizedFoodList} </div> </Box> );}
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
While useMemo is generally preferable, there are still valid use cases for React.memo.
React.memo is appropriate when:
- You have a pure component that is used in many places with the same optimization needs
- You’re creating a reusable library component where the optimization should be built-in
- 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.
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

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
- React Documentation: useMemo & React.memo