The React Show

A podcast focused on React, programming in general, and the intersection of programming and the rest of the world. Come join us on this journey.

Listen

By Thomas Hintz

When should you use React.memo and useMemo?

To improve performance of your functional components you can use React.memo and useMemo. React.memo is a higher-order component (HOC) that wraps your functional component. It then memoizes its rendered output. useMemo is a hook that gets run during the render stage and also memoizes its output based on an array of dependency inputs. Both of these methods require you to program in a "pure" style. This means given the same prop values you always render the same output. useState and useContext hooks will also trigger a re-render for components that use React.memo.

If you're working with function based components you should read on but if you are working with class based components you can learn how to achieve the same thing with React.PureComponent in my article "When should you use React.PureComponent?".

React.memo

const Component = React.memo(MyComponent, areEqual);

React.memo takes one required argument, your React component, and one optional argument, a comparison function.

Eligibility

// Example 1
// Example of a "pure" component
// Returns an H1 denoting whether the pancake was over-cooked

const CookedPancake = ({ batter, bakingTime, maxTime }) => {
    if (bakingTime > maxTime) {
        return (<h1>{batter} Over-cooked!</h1>);
    }
    return (<h1>{batter} Cooked Just Perfect!</h1>);
};

In the first example we just define a pure component and you can see that if the props are the same the rendered output will be the same and the component doesn't reference outside state or use random numbers. This is the first step in determining if a component is eligible for React.memo.

The next step is determining if the same props are passed in to the component multiple times or not. Like let's say we are using the CookedPancake component in an app that runs a simulation of baking times. The app starts simulating baking times from 0 to 10 incrementing the baking time by 1 for each iteration. In this case bakingTime is not a good candidate for React.memo because the bakingTime prop is different every time React goes to render the component so memoization will provide no benefit.

On the other hand let's say we have multiple different types of pancake batters in a list and the user is able to re-arrange the order in which they cook the pancakes. The prop values will be reused every time React re-renders the list of CookedPancake components. And since React may re-render each CookedPancake whenever the order of the list changes this means we have the right conditions for React.memo.

Usage

// Example 2
// Memoized component

const CookedPancake = React.memo(({ batter, bakingTime, maxTime }) => {
    if (bakingTime > maxTime) {
        return (<h1>{batter} Over-cooked!</h1>);
    }
    return (<h1>{batter} Cooked Just Perfect!</h1>);
});

// Render CookedPancake with the same prop values.
const MyPancakes = () => {
    return (
        <>
            <CookedPancake batter='Swedish'
                           bakingTime=1
                           maxTime=1.5
            />
            <CookedPancake batter='Swedish'
                           bakingTime=1
                           maxTime=1.5
            />
        </>
    );
};

In Example 2 we add React.memo to the component definition and add the MyPancakes component that renders two Swedish CookedPancake. When React renders the first CookedPancake it will run the component's render code like normal but when it goes to render the second CookedPancake it will find a stored result matching the same prop values and instead of running the CookedPancake render code it will just use the result of its stored run (from the first CookedPancake render). It should also be noted that if CookedPancake had any child components their render code would also not be run since their output is a part of the memoized result. This means you can stop React from rendering an entire portion of your component tree with React.memo.

Pitfalls

// Example 3
// WRONG, this will not behave as expected

// A button that takes an onClick handler and some text.
const Button = React.memo(({ onClick, text }) => {
    return (<button onClick={onClick} text={text});
};

// Use the Button component and log to console when clicked.
const LogButton = () => {
    return (
        <Button onClick={() => console.log('Log button clicked!')}
                text='Log'
        />
    );
};

const App = () => {
    return (
        <>
            <LogButton/>
            <LogButton/>
        </>
    );
};

It's important to remember that React.memo use shallow comparison of props so passing anonymous functions or Javascript literals will probably not do what you want, as we can see in Example 3. In this example we create a simple button that allows an onClick handler to be set and some text for the button. Then we create a LogButton that just logs to the console when it is clicked (and this is where the bug is too). The App component just includes two LogButton components. If this behaved the way we might expect then we would expect React to not re-render LogButton when it gets to the second LogButton in App because it appears as if the props to Button are the same. But, in fact, they are not the same at all. The onClick handler is an anonymous function that Javascript creates every time it renders that code. So even though what the function does is constant a new Javascript object is allocated every time. This means that the memoization function always thinks the props have changed and will always re-render the component. Not what we want.

// Example 4
// Re-write LogButton to work with Button's React.memo

const logButtonClicked = () => console.log('Log button clicked!');

const LogButton = () => {
    return (
        <Button onClick={logButtonClicked}
                text='Log'
        />
    );
};

In Example 4 we can see that moving the definition of the logging function outside the render path fixes the issue because now the same function object is always passed to Button.

// Example 5
// WRONG, does not work as expected

// We change Button to take text as an object
const Button = React.memo(({ onClick, attributes }) => {
    return (<button onClick={onClick} text={attributes.text});
};

const LogButton = () => {
    return (
        <Button onClick={logButtonClicked}
                attributes={{ text: 'Log' }}
        />
    );
};

Let's say we wanted our button to take an object of attributes that we could use instead of adding a new prop for every attribute of our button. This might seem straightforward at first, as we can see in Example 5 with { text: 'Log' }. But this suffers the same flaw as Example 3 with the anonymous function. Every time React renders LogButton it will create a new object for attributes breaking the memoization. In this case the fix is the same as for the anonymous function: don't create object literals in the render path.

areEqual

What if you want to use React.memo but the provided shallow comparison is not enough for your use case? Maybe there is an inexpensive way you can do a deeper comparison? Well React thought of that too and allows you to specify a second argument to React.memo: areEqual.

// areEqual signature
function areEqual(prevProps, nextProps) {}

// Usage with React.memo
React.memo((props) => ..., areEqual);

areEqual is a function you provide to React that receives prevProps and nextProps. You return true if passing both prevProps and nextProps to your component would yield the same result. Note that this is the opposite of how shouldComponentUpdate works where returning true would cause a re-render. Inside areEqual is where you would do a deep(er) comparison.

Children

Also make sure that all child components of your memoized component follow the same rules otherwise they won't re-render when you expect them to.

useMemo

const memoizedValue =
    useMemo(() => expensiveComputation(x, y), [x, y]);

useMemo is just a memoization function implemented as a hook that React provides. It takes two arguments. The first is a function that performs an expensive computation and the second are the variable dependencies of that function.

// Example 6
// A pancake component visually representing the pancake's height.

const YummyPancake = ({ batterAmount, batterViscosity }) => {
    const thickness = useMemo(
        () => calculateThickness(batterAmount, batterViscosity),
        [batterAmount, batterViscosity]);

    return (
        <div className='pancake' style={{ height: thickness }} />
    );
};

Here we define a YummyPancake component that renders a div with its height determined by the calculated thickness of the pancake. Since calculating the thickness of a pancake is a very expensive operation and we might be making many pancakes with the same batter amount and batter viscosity (very precise chefs) we take advantage of useMemo.

In the example calculateThickness is called only if batterAmount and batterViscosity, the dependent variables, do not have the same values of any other time that calculateThickness was called. This means that all dependencies of your expensive function should be in the dependency array. If they are not then React may not call your function to calculate a new value when you are expecting it to. You'll also notice that calculateThickness is inside an anonymous function. This delays evaluation so that useMemo can decide if and when to execute calculateThickness.

In many ways useMemo follows the same rules and guidelines as React.memo. useMemo only does a shallow comparison on its dependencies and expects your expensive function to be pure. So be careful not to use object literals or anonymous functions in your dependencies.

In summary, you should use React.memo and useMemo when rendering is a bottleneck and you are working with pure functions, props, and arguments. And if you want to learn more about high performance React programming subscribe to the email list or the atom feed.