June 16, 2020

Using `React.memo` to Avoid Unnecessary Rerenders

edit

In my last post, I talked about the concept of memoization and how the technique is used to avoid calculating a previously calculated result of a function. I also made mention of the two memoization functions in React. This post is going to focus on React.memo and how to use it to avoid unnecessary rerenders of React components.

When Hooks were introduced to React in late 2018, we were also given some new top-level functions in the React API. One of those new functions is React.memo. It is the function component equivalent of using PureComponent for a class component.

If your component is a pure function, i.e. given the same inputs you get the same output, you can wrap the component in React.memo to prevent the component from rerendering if props haven’t changed. Let’s create a simple Profile component that we can memoize to demonstrate.

function Profile({ name, location }) {
  return (
    <div>
      <div>{name}</div>
      <div>{location}</div>
    </div>
  )
}

const MemoizedProfile = React.memo(Profile)

If I add a few styles, give each component the same props, and render both of these components side by side, they will look perfectly identical.

Great. That’s what we expect. There’s nothing fancy about the initial render of a component with props. We give it a set of values, we expect the output to be the correct result.

Where React.memo becomes useful is when we introduce a parent component. In React, if a component updates, then it rerenders everything in that component. This is necessary. The new state of the component may affect anything rendered below it. This rerendering happens regardless of whether the child component has experienced a change or not. That’s where React.memo comes into play.

A memoized component will skip rendering if its props have not changed. We can easily demonstrate this, and I get to share one of my simple little hacks for detecting unnecessary rerenders in the process.

We’re going to change Profile slightly. We’re going to give it a random background color every time it renders. This is simple to write and do. Just make sure to remove it when you’re ready to ship your code.

const random255 = () => Math.floor(Math.random() * 255)
const randomRGBA = () => {
  const r = random255()
  const g = random255()
  const b = random255()

  return `rgba(${r}, ${g}, ${b}, 0.3)`
}

function Profile({ name, location }) {
  return (
    <div style={{ backgroundColor: randomRGBA() }}>
      <div>{name}</div>
      <div>{location}</div>
    </div>
  )
}

const MemoizedProfile = React.memo(Profile)

Now, every time the component renders, a new background color will be applied. Let’s create a Parent component that has a frivolous state change that should cause the Profile components to rerender.

function Parent({ children }) {
  const [, setState] = React.useState(false)
  const forceUpdate = () => setState(x => !x)

  return (
    <>
      <button onClick={forceUpdate}>Force Update</button>
      <div
        style={{
          display: 'grid',
          gridGap: bs(1),
          gridTemplateColumns: '1fr 1fr',
          marginTop: bs(0.5),
        }}
      >
        <Profile name="Kyle Shevlin" location="Portland, OR" />
        <MemoizedProfile name="Kyle Shevlin" location="Portland, OR" />
      </div>
    </>
  )
}

Notice that when you click “Force Update”, only the regular Profile component updates. The memoized version does not. In the right scenarios, this can become a real performance boost.

What Does “Same Props” Mean?

We’re going to make a small change to our Profile component again to demonstrate an important concept.

// Pay attention to how we've changed props
function Profile({ person }) {
  const { name, location } = person

  return (
    <div style={{ backgroundColor: randomRGBA() }}>
      <div>{name}</div>
      <div>{location}</div>
    </div>
  )
}

We’ve moved name and location into a person object. You can imagine this scenario happening often in any React app. You have an array of objects and you pass the whole object as a prop to a child component. How does this change how our memoized component works? Let’s update our Parent component to now pass these objects into the component instead.

function Parent({ children }) {
  const [, setState] = React.useState(false)
  const forceUpdate = () => setState(x => !x)

  return (
    <>
      <button onClick={forceUpdate}>Force Update</button>
      <div
        style={{
          display: 'grid',
          gridGap: bs(1),
          gridTemplateColumns: '1fr 1fr',
          marginTop: bs(0.5),
        }}
      >
        <Profile
          person={{
            name: 'Kyle Shevlin',
            location: 'Portland, OR',
          }}
        />
        <MemoizedProfile
          person={{
            name: 'Kyle Shevlin',
            location: 'Portland, OR',
          }}
        />
      </div>
    </>
  )
}

Now let’s give this set of components a try. Click the “Force Update” button below and pay attention to what updates.

Both components update! Why does this happen?

Even though the props are the same values with every update, the object passed into each component on every render is a new object. Objects are strictly compared by reference, not by their shape of keys and values. The simplest way to demonstrate this is to put this into your browser console and see the result

{} === {}

This is the same check that React.memo and PureComponent make and is called a “shallow comparison” of objects. Because we’re creating new objects with each render to pass into our Profile components, this comparison always results in React needing to update. Hence why they both rerender every time that the Parent component updates.

To drive this point home, let’s store these objects as refs inside of the component. A ref in React is an object that will not lose its reference across renders. By storing an object as a ref, we guarantee that we get the same object (referentially) with each render.

function Parent({ children }) {
  const [, setState] = React.useState(false)
  const forceUpdate = () => setState(x => !x)
  const personRef = React.useRef({
    name: 'Kyle Shevlin',
    location: 'Portland, OR',
  })

  return (
    <>
      <button onClick={forceUpdate}>Force Update</button>
      <div
        style={{
          display: 'grid',
          gridGap: bs(1),
          gridTemplateColumns: '1fr 1fr',
          marginTop: bs(0.5),
        }}
      >
        <Profile person={personRef.current} />
        <MemoizedProfile person={personRef.current} />
      </div>
    </>
  )
}

Now our memoized component is back to working as we expect. Because the object’s reference is the same with each render, it skips rerendering. It’s important to understand this fact, as it might have an impact on how you design your components.

React.memo’s Second Argument

Using a ref like we just did is not how I would recommend handling this situation. It was simply to demonstrate a point. This section is also primarily to demonstrate a point. Use what I’m about to show you sparingly. Following general best practices will keep you from needing this feature often.

Suppose we must pass an object into a component, and we know that often these objects will be “deeply equal” even if they aren’t the same reference. We can use the second argument to React.memo to supply an areEqual function that determines if a component should rerender or not. This function is similar to implementing shouldComponentUpdate on a class component, in that we compare the previousProps to the nextProps, but opposite in what we want to return from the function. We want the areEqual function to return true when the component should not update. Let’s update how we memoize Profile accordingly:

const personsAreEqual = (prevProps, nextProps) => {
  return (
    prevProps.person.name === nextProps.person.name &&
    prevProps.person.location === nextProps.person.location
  )
}

const MemoizedProfile = React.memo(Profile, personsAreEqual)

Now, if we use that in our example, we’ll see that even without refs, our components behave as we expect.

Summary

We can improve the performance of our React apps by avoiding unnecessary rerenders with React.memo. We have to understand what it means to pass the “same props” to our components, though, in order to achieve this performance boost.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Array.reduce()
Array.reduce()
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.