Use React.memo to optimize re-rendering

1 Agu 2020 · 9 min read

Link copied successfully

In the previous article, I discussed how the rendering process occurs in React. I suggest you read the article first to make it easier to understand this article.

The React Framework gets its name from its nature which reacts to changes in state or props in the application. When changes occur, React will re-render the components and then implement the changes to the DOM.

However, sometimes a component can be re-rendered when it doesn't need it.

function Title() {
  console.log("re-rendered");
  return <h1>Counter</h1>;
}

function Counter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount((v) => v + 1);

  return (
    <div>
      <Title />
      <p>{count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

In the code above I made two simple React components, the Title component that contains text, and Counter which has the feature of adding numbers.

When executed, any state changes to the Counter component will trigger the logging of the word "re-rendered" in the console. Why does this happen? whereas the Title component only has static text without state or props.

The answer is because when a state or props changes in a component, React will create a new Virtual DOM from the changed component.

Virtual DOM is a normal Javascript object that represents the DOM. React creates a Virtual DOM at the initial render of the application, then continues to make it when the state or props changes.

Every Virtual DOM that is made will be compared with the previous one, to see if there are any changes that will affect the DOM. This process is called diffing, which determines what must be changed in the DOM.

Let's examine the initial Virtual DOM condition at Counter component before we press the increment button first.

// counter component

{
  type: 'div',
  props: {
    children: [
      {
        type: Title,
        props: null,
      },
      {
        type: 'p',
        props: {
          children: 0,
        },
      },
      {
        type: 'button',
        props: {
          onCLick: increment,
          children: 'Increment',
        },
      },
    ],
  },
};

After pressing the increment button, there will be a state change and React creates a new Virtual DOM that reflects the change.

// new counter component

{
  type: 'div',
  props: {
    children: [
      {
        type: Title,
        props: null,
      },
      {
        type: 'p',
        props: {
          children: 1, // => change
        },
      },
      {
        type: 'button',
        props: {
          onCLick: increment,
          children: ['Increment'],
        },
      },
    ],
  },
};

Even though the Title component has not changed, <i>because this is a new object, the props they have are also considered new</i>, and React needs to re-render the component to double-check whether the new props will change the shape of the Title component (for example, changing JSX in the component) or not.

This behavior of React can be changed using React.memo. It works by memoizing the render result of a component, and will only re-render it if there is a change in the component's props.

Let's make a little changes at the Title component that we have

const Title = React.memo(function () {
  console.log("re-rendered");
  return <h1>Counter</h1>;
});

After that, each time you press the increment button, the log of the word "re-rendered" will only appear once, because there are no changes to the props (and indeed the Title component doesn't have any props).

Of course, the above example is not best-practice for using React.memo. Re-render one simple component will have no impact on application performance.

A case study

To better understand the use of React.memo, I want to make a simple case study. I will make a form component iterated over n times, with the target that every state change will only render the related component.

To begin, we first create a component for the form with 3 fields: firstName, lastName, email. Just name the file IdentityForm.js

import React from "react";
import { useCountRenders } from "./useCountRenders";

export const IdentityForm = ({ data, index, handler }) => {
  // custom hooks to track counts of re render happened
  useCountRenders(data, index);

  return (
    <div className="form-group">
      <b>{index + 1}. </b>
      <input
        type="text"
        placeholder="First Name"
        value={data.firstName}
        onChange={(e) => handler(e.target.value, "firstName", index)}
      />
      <input
        type="text"
        placeholder="Last Name"
        value={data.lastName}
        onChange={(e) => handler(e.target.value, "lastName", index)}
      />
      <input
        type="text"
        placeholder="Email"
        value={data.email}
        onChange={(e) => handler(e.target.value, "email", index)}
      />
    </div>
  );
};

In the component above, we have 3 props that are: data contains a value for each field, index gives the number of the field, and will be used as an identifier for data changes, handler is a function to take care of data changes.

In addition, we also make simple custom hooks to help count the number of times the component is re-rendered. The following code is for useCountRenders.js

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

export const useCountRenders = (data, index) => {
  const renders = useRef(0);

  useEffect(() =>
    console.log(
      "renders:",
      renders.current++,
      "times, for form component number",
      index + 1
    )
  );
};

Then for the main components where state and logic are located, create a file with the name App.js containing the following code

import React, { useState } from "react";

import { IdentityForm } from "./IdentityForm";
import "./styles.css";

export default function App() {
  const [states, setStates] = useState(
    Array(5).fill({
      firstName: "",
      lastName: "",
      email: "",
    })
  );

  const handler = (value = "", key, i) => {
    setStates((states) =>
      states.map((data, index) =>
        index === i ? { ...data, [key]: value } : data
      )
    );
  };

  return (
    <div className="App">
      {states.map((state, index) => (
        <IdentityForm
          key={index}
          data={state}
          handler={handler}
          index={index}
        />
      ))}
    </div>
  );
}

The preparations are finished, but it will be better if I explain some of the code we have made.

// ...
const [states, setStates] = useState(
  Array(5).fill({
    firstName: "",
    lastName: "",
    email: "",
  })
);
// ...

Here we use useState hooks to store the data we have. The default data is an array containing 5 objects, each object contains data for each IdentityForm component.

// ...
const handler = (value = "", key, i) => {
  setStates((states) =>
    states.map((data, index) =>
      index === i ? { ...data, [key]: value } : data
    )
  );
};
// ...

This is the handler function that we will use to process state changes in each IdentityForm component. It works by doing map in the current state and making changes to the data corresponding to its index.

{
  states.map((state, index) => (
    <IdentityForm key={index} data={state} handler={handler} index={index} />
  ));
}

Finally, in the JSX part, we also do map to iterate all data to the IdentityForm component.

If the above code runs successfully, we will find the following displayed in the browser. Note the console, log that appears is caused by the initial render of each IdentityForm component.

post three image

Then, when we add the first two characters to one of the forms, the log will add as follows

post three image

Can be seen in console, changes to the first component make the other 4 also re-rendered.

That might not be a problem if we only have 5 or 20 components. But you can try changing the number of the array in states to become, for example, 5000, and fill in any form. You will find the application to be very slow, or maybe crash.

React.memo implementation

Alright, now we take out the ultimate weapon React.memo. Just change the IdentityForm component a bit like this

import React from 'react';
import { useCountRenders } from './useCountRenders';

export const IdentityForm = React.memo(({ data, index, handler }) => {

  // ... content
  );
});

Then we see again whether there is a change?

post three image

What? Why isn't there a change? even though we clearly added React.memo to the IdentityForm component.

A good callback

Remember the 3 props that we give to the IdentityForm component, one of which is handler, a function for processing changes in data on the component.

In the React functional component, every function that is in the component will be re-created on each re-render. So, the handler function that we make as props, will always be a new function, which triggers the re-rendering of the IdentityForm component, even if it uses React.memo.

We can avoid this re-creation by moving the function out of the component. However, our handler function currently uses setStates to change the data in useState hooks, so we cannot put it outside the component.

it's important to avoid creating additional components in the React functional component because these components will be re-created every re-render. Make these additional components outside the main component, or in another file.

To overcome this problem, React gives us another very useful hooks, namely useCallback. This hook works by remembering an instance of function, and only re-create it when the dependency has changed.

So, if you use React.memo by emptying the dependency, we get an instance of function which is always the same. Change the handler function on App.js like this.

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

// ...

const handler = useCallback((value = "", key, i) => {
  setStates((states) =>
    states.map((data, index) =>
      index === i ? { ...data, [key]: value } : data
    )
  );
}, []);

// ...

Let's try filling in the modified form, again

post three image

Voila! We did it. Re-render only occurs in component number 1 only. Not yet satisfied? We can test again by increasing the number of IdentityForm components. Just change the number 5 in the default value in useState to 1000 or whatever you want (my suggestion is not more than 5000).

post three image

We made it again. Only component number 983, where there is a state change that is re-rendered, Congratulations!

You can see the full code of this case study here. That's it for this article, if you have feedback or questions don't hesitate to contact me on Twitter.

Reference

1. Kent C. Dodds - One Simple trick to optimize React re-renders

Emot's Space © 2025