Optimise React with useMemo and React.memo

Optimise React with useMemo and React.memo

There comes a time when we have to worry about more than just making sure our applications work, but that they work optimally. When using react, we have certain tools at our disposal to make sure our applications are optimised. In this article, I will demonstrate how to achieve this using React.memo and the useMemo hook.

Rendering

Before we dive into the use of these methods, let’s first establish a basic understanding of how react components are re-rendered.

Components in react will re-render when there is a change in their state and/or their props.

Child components will also re-render whenever their parent component is re-rendered. Even when the child’s state/props haven’t changed.

Memoization

The second concept we need to understand is memoization as it is central to how React.memo and useMemo work.

Memoization is the practice of caching the results/outputs of expensive functions or operations and returning these cached results the next time identical input is provided.

This optimises our program by allowing us to skip costly computations entirely if the provided inputs have already been used before.

React.memo and useMemo make use of this concept to determine whether components should be re-rendered or values should be re-computed respectively.

useMemo

Let’s start with useMemo. This is a react hook that we use within functional components in order to memoize values (especially from expensive functions).

useMemo takes 2 parameters: a function that returns a value to be memoized, and an array of dependencies. Dependencies are the variables that determine whether the memoized value should be recomputed.

In other words, as long as the dependencies haven’t changed, do not re-run the function to update the memoized value. Since the dependencies are contained in an array, you can have multiple dependencies for useMemo.

Do note only ONE of the dependencies in the dependency array needs to change in order to trigger the execution of the function/operation.

Now let’s look at an example of useMemo in action.

First, let’s write some simple application code that does not make use of useMemo.

const User = ({ greeting }) => {
  console.log(greeting)
  return (
    <div>
      <p>{greeting}</p>
    </div>
  )
}

Here we have a User component that simply renders a string contained in the greeting prop. This string is also logged to the console. You will see in just a moment why this is important.

Next, let’s define the App component:

const App = () => {

  const [name, setName] = useState('Michael')

  const greet = () => {
    return `Hello, ${name}`
  }

  const greeting = greet()

  return (
    <div className="App">
      <div>
        <form onSubmit={(event) => {
          event.preventDefault()
          const data = new FormData(event.target)
          setName(data.get('name'))
        }}>
          <input type='text' name='name'/>
          <input type='submit' value='Change name'/>
        </form>
      </div>
      <User greeting={greeting} />
    </div>
  )
}

The app component contains a function called greet that performs the unfathomably slow operation of returning a greeting based on the current name in the state (which is defaulted to ‘Michael’).

We have a greeting constant that is computed by calling the greet function. This is the string that is passed to the User component.

We also have a form that when submitted, updates the name in the App component’s state.

When we run this application, nothing out of the ordinary happens. Submitting the form updates the name, which causes App components to re-render. This causes the greeting to be updated and finally the User component re-renders with the updated prop.

For the sake of this example, let’s imagine that the greet function is a very expensive function that eventually returns our greeting. How can we make use of useMemo to prevent it from being executed on every re-render?

We can memoize the greeting by updating it to the following:

const greeting = useMemo( () => {
    return greet()
  }, [])

Now we only compute the value of greeting when the dependencies update.

But hold on a minute, the dependency array is empty. What happens in this case?

If you’re familiar with the useEffect hook, you’ll know that to mimic the functionality of componentDidMount, we pass an empty dependency array so that it executes once upon the first render.

This is exactly what happens here. This value will be computed once on the first render and will be the same for all subsequent renders. No matter how many times the name changes, the value of greeting will not change.

Now let’s use it a little more practically. We want to re-compute the greeting every time name changes. But because doing this basically renders useMemo useless, let’s add a condition to the name update:

We will only update the name in the state if the submitted name contains the string ‘Kelvin’ in it. So let’s update the form’s onSubmit function to the following:

<form onSubmit={(event) => {
          event.preventDefault()
          const data = new FormData(event.target)

          let name = data.get('name')
          if (name.toLowerCase().includes('kelvin')) setName(name)

          setCount(count + 1)
        }}>
          <input type='text' name='name'/>
          <input type='submit' value='Change name'/>
</form>

Now we’re conditionally updating the name so it makes sense to memoize the greeting depending on the name, since it’s not updating on every submit. I’ve also added a count variable in state that increments every time the form is submitted just to force the App component to re-render regardless of wether name is updated.

Now we can update the useMemo hook to the following:

const greeting = useMemo( () => {
    return greet()
  }, [name])

The only difference here, is that we’ve added the dependency of name to it. Every time name changes, only then will the greeting be recomputed.

When we run this app, we can see that on the User component, the greeting doesn’t change when the input doesn’t contain ‘Kelvin’ in it. On those instances, the memoized greeting is still being used.

Remember that console.log statement we had in our User component? If you look at your console, you’ll notice that the greeting gets printed whether the memoized value is being used, or a new value is calculated.

It appears we are preventing the greeting from being re-computed on certain instances, but the component is always being re-rendered. Why is this?

The answer is simple: Even though the prop doesn’t change in those instances, the component still gets re-rendered simply because the parent has been re-rendered thanks to the count increment.

So what if the rendering of a child component is in itself expensive and we want to make sure we prevent re-rendering when props haven’t changed even if the parent has re-rendered?

This is where React.memo comes in!

React.memo

As mentioned before, React.memo prevents a component from re-rendering unless the props passed to it have changed.

To put this into practice, let’s update the User component to the following:

const User = React.memo(({ greeting }) => {
  console.log('User component rendered')
  return (
    <div>
      <p>{greeting}</p>
    </div>
  )
})

We have wrapped the component with React.memo. We’ve also updated the log statement to let us know when the User component has rendered, just for extra clarity.

Add the following statement in the App components body before the return statement in order to indicate whenever the App component has been re-rendered:

console.log('App component rendered')

Run the application and you will notice that ‘Hello, Michael’ is displayed on the page. When you enter any name besides Kelvin, the name is not updated in state. Count is always updated in state just as before.

The difference this time is that the User component will not be re-rendered as you can see from the console logs.

Why is this? Well, when the name is updated to any value other than ‘Kelvin’ the value of greeting is not updated. The App component still re-renders because the value of count is updated.

This re-rendering of the App component does not affect the child component User as React.memo prevents it from re-rendering due to the fact that the value of the props (in this case, greeting) hasn’t changed.

Change the name to ‘Kelvin’ and you’ll notice that this time, the name is updated in App state, which causes the value of greeting to be updated, which in turn allows the User component to be re-rendered.

Manual render

As we’ve seen, React.memo prevents a component from re-rendering when the props haven’t changed.

React.memo uses shallow comparison to compare the previous set of props to the next incoming set of props in order to determine whether the component should be re-rendered.

If shallow comparison is not sufficient for your needs, as props tend to contain very complex objects in larger applications, you can pass a second optional argument to React.memo: A function that takes previous props and next props as parameters which allows you to manually determine whether the component should be re-rendered.

To implement this, let’s update the User component:

const User = React.memo(({ greeting }) => {
  console.log('User component rendered')
  return (
    <div>
      <p>{greeting}</p>
    </div>
  )
}, (prevProps, nextProps) => {
  if (prevProps === nextProps) return true
  return false
})

Note that this function should return false if you DO want the component to re-render and true if you want to skip the re-render.