React components performance optimization

React components performance optimization

How to use memo, useMemo and useCallback

Introduction:

In a React application, components are rendered…usually a lot. Improving performance includes preventing unnecessary renders and reducing the time a render takes. React comes with tools to help us prevent unnecessary renders: memo, useMemo, and useCallback.

No change, no re-render:

When the parent's state (or props) changes, react will re-render all the children components even though their props or state are the same.

Have a look at the following code:

import React, { useRef, useState } from "react";

const Cat = ({ name }) => {
    console.log(name, " rendered!");
    return <li>{name}</li>;
};

const App = () => {
    const [cats, setCats] = useState(["Boggy", "Siana"]);
    const inputRef = useRef();

    const handleSubmit = (event) => {
        event.preventDefault();
        setCats([...cats, inputRef.current.value]);
        inputRef.current.value = "";
    };
    return (
        <>
            <form onSubmit={handleSubmit}>
                <input
                    ref={inputRef}
                    type="text"
                    required
                />
                <button type="submit">Add cat</button>
            </form>
            <ol>
                {cats.map((name, index) => <Cat key={index} name={name} />)}
            </ol>
        </>
    );
}

Let's add a new cat Picka and see what the console logs:

Boggy  rendered!
Siana  rendered!
Picka rendered!

Every time we add a new cat and update the state, react re-renders all the children components:

The Cat component is a pure component: same output given the same props. We don’t want to re-render a pure component if the properties haven’t changed. React provides us with a solution: The memo() function can be used to create a component that will only re-render when its properties change.

/* import the `memo` function */
import React, { memo, useRef, useState } from "react";

const Cat = ({ name }) => {
    console.log(name, " rendered!");
    return <li>{name}</li>;
};

/* wrap the `Cat` component within `memo` */
const MemoCat = memo(Cat);

We’ve created a new component called MemoCat. MemoCat will only cause the Cat to render when the props change.

Then, you can replace the cat component with MemoCat.

...
{cats.map((name, index) => <MemoCat key={index} name={name} />)}
...

Now, every time we add a new cat, we see only one render in the console:

picka rendred!

useCallback:

Now, what if we need to pass a function to the Cat component along with the name prop? Ok, let's modify the Cat component:

const Cat = ({ name, meow = f => f }) => {
    console.log(name, " rendered!");
    return <li onClick={() => meow(name)}>{name}</li>;
};
const MemoCat = memo(Cat);

Note: the f => f is a fake function that does nothing. It is used to avoid errors in case we didn't provide a meow property to the Cat component.

Now, let's modify the App and provide a meow prop:


const App = () => {
    const [cats, setCats] = useState(["Boggy", "Siana",]);
    const inputRef = useRef();
    // create a function that we will pass to the Cat component as a prop.
    const meow = (name) => console.log(name, 'meowed!');

    const handleSubmit = (event) => {
        event.preventDefault();
        setCats([...cats, inputRef.current.value]);
        inputRef.current.value = "";
    };
    return (
        <>
            <form onSubmit={handleSubmit}>
                <input
                    ref={inputRef}
                    type="text"
                    required
                />
                <button type="submit">Add cat</button>
            </form>
            <ol>
                {cats.map((name, index) => <MemoCat 
                    key={index} name={name} meow={meow} />
                )}
            </ol>
        </>
    );
}
export default App;

Let's retry adding a cat named 'picka' to the list and see what is logged in the console:

Boggy  rendered!
Siana  rendered!
picka  rendered!

Unfortunately, the magic of memo is gone. But why???

Every time we update the state, the App re-renders and the meow function is re-created. For React, the new meow function prop is different from the previous one. So it re-renders the Cat component.

To solve this problem, we have to 'memoize' (or simply remember) the meow function definition between re-renders. That's why we use useCallback hook.

Let's import useCallback:

import {useCallback} from 'react'

and use it in our App component to:

const meow = useCallback((name) => console.log(name), [])

It works as expected when adding a new Cat!

Heavy calculation? cache it:

Rendering takes time and resources, especially if your component is doing heavy calculations, like looping over a large array or doing expensive computations.

React provides us with the useMemo hook, which let's us cache the result of a calculation between re-renders.

useMemo takes two parameters, the 1st one is a function, and a dependency array of values needed in the calculation, it will re-run the function only if the dependency array changes. if the dependency array didn't change and the component re-renders, useMemo won't re-calculate the value and it will return the one from the previous render instead.

Bonus: How to determine if your component is doing heavy calculations:

To know if your component is doing heavy tasks, you may need to measure the time spent on a piece of code using console.time().

console.time('filter array')
const filteredArray = useMemo(() => {...}, [deps])
console.timeEnd('filter array')

if your function needed a significant amount of time to execute (say 1ms or more), you may need to use useMemo

That's all for today! I hope you enjoyed reading this article.

References: