React memoization in action

Memoization is something which comes in handy for performance optimisations. I will try to make it seem less intimidating through this blog, and show you how you can use memoization to your advantage through a simple react application.

In this application, we will simulate how a car works and see memoization in action.

Creating the main App component

After generating a project using create-react-app, you can go into your src/App.js file. Here, we will create a component which will act as the main “car”, and will look over its different workings.

We will assume that our car will have basic functions of accelerating, braking and showing speed on the odometer.

Let us first clear the default boilerplate code from App.js, so that we have a clean slate to start with:

We have added some basic inline styling to the blank page, in anticipation of the components which we will add.

Let us next add the speed state, which we will use to show the speed of the car.

We will be using App.js to distribute this speed state to three child components:

  • Engine
  • Brake
  • Odometer

These will be very simple components, as we will soon see.

First, let us create a new Engine.js file directly in the src folder, alongside App.js:

This component expects to get a handleAcceleration prop from the App component.

This prop will hold a function, which will just increment speed state by 1 on every click.

Let us first add such a function to App.js, to handle this state update. We will give it the same name as the prop we are passing:

Similarly, we will also add a method to set the speed back to zero, which will act as the “braking” mechanism:

We will next create our Odometer and Brake components. The Odometer will get the speed as a prop and simply display it, while the Brake component will only get the handleBrake function as a prop.

Creating the Odometer.js file first, in the src folder alongside App.js:

Followed by the Brake.js file, also alongside App.js:

Now all that is left to do is import these into App.js, and render them into the browser.

We have passed the speed state as a prop to Odometer, and the handleBrake method with the same prop name to Brake.

With this, we run yarn start and view the application in the browser. If everything went as expected, you should have the following view:

App running on localhost:3000 (on Safari)

With that, we can check to see if everything is working as intended.

Working of our very simple “Car”

Great, now we can look at all the problems associated with this “Car”.

Let us add a console.log statement to our Brake component first, to see the first area where we could optimise our app using memoization with react.

In your src/Brake.js file, add the following line:

Now we will click on the accelerate button, only to find out something very troubling. The “brakes also work” every time we accelerate, which would of course be very troublesome for an actual car. Check out the logs below:

Notice the browser console at the bottom

Why does this happen? Whenever we click the accelerate button, we update the speed state of the parent: App.js, and therefore cause it to re-render.

This also results in updating the Brake component since it is also its child component, even though we did nothing to it.

This is exactly where memoization can help us.

To make sure our Brake component does not unnecessarily re-render, we can think of wrapping it in the React.memo HOC.

We can do this as follows:

The react docs say that it can be used to prevent re-renders when the props stay the same, right?

But let us see what actually happens:

The Brake component re-renders despite React.memo

We see that the Brake component is still re-rendering! Why is this so?

This is because of the nature of the function that we are passing as a prop to Brake, from our App component.

In our App.js file, check out the handleBrake function that updates the speed state:

This function is declared again, every time that the App component re-renders, and hence is treated as a new version of the prop that we pass into the Brake component. This is because the reference to the function changes, and React.memo only does a shallow comparison by itself.

React.memo would prevent re-renders for simple, unchanging props being passed in like strings and numbers, but for things like functions and objects we need to use a little more firepower.

To ensure that this component does not cause a re-render in the Brake component, we can use the useCallback hook to wrap around it in App.js:

Now if we have a look at our application, we will see that the Brake component is only rendered in the initial render cycle:

No re-renders after using useCallback along with React.memo

The empty array is the array of dependencies which we can use to update the memoized value. In case there aren’t any dependencies, like in our case, we can leave it blank.

useMemo also works similarly, only that it returns a memoized value, by using a “creator” function.

For this, we will need to return our function instead of wrapping it directly like we could with useCallback:

Which will give us the same result. When seen side by side:

The additional function that we need to pass to useMemo is a “create” function which is used to create the memoized value.

This is not required for useCallback, where you can memoize the callback directly by wrapping it. Hence the more apt name, since you can pass your callback directly to be memoized.

Earlier, we talked about how React.memo would not suffice in case of functions and objects, since they are re-declared (their reference value updates) on every re-render of the parent component.

We can however, use it directly for simple props like strings and numbers, which are not passed by reference. Hence, as they are passed by value, as long as the value remains the same React.memo will memoize it directly.

Let us add a speed limit component to illustrate this, which will take some speed limit values.

We can create it in the src folder beside App.js too:

We will pass it max and min props to display the limits. These will be plain numbers. We have also added a console log to see if the component re-renders.

Let us import it and use it in App.js:

You might be wondering if we might need useMemo in this case as well, since we are passing properties of an object as limits.max and limits.min, right?

But notice that the limits object is outside of the App function, and hence will not be a part of re-renders. This is another strategy used to improve performance: shift all objects and functions that do not depend on the component, outside of it. This way they are automatically skipped in re-renders.

Ok, back to our browser. Here is what we get:

Limits is re-rendered every time even for unchanging values

This is because although we are passing static values, we haven’t wrapped the Limits component in React.memo, so let’s do that next. In your Limits component:

And now if you open the browser, then after the initial render, you will not see the console log appearing in your browser:

The console log no longer shows additional logs for the Limits component

After seeing memoization in action, you might be tempted to use it everywhere. But comparing props in very simple & straightforward cases might prove to be more expensive sometimes instead of simply re-rendering a component.

This is because the comparison computation would itself take some amount of memory, and there are also cases where frequent re-renders might just be in the nature of the component. If every component had this condition, the effects would ripple throughout your application.

Take our Odometer component for example, which will re-render every time that brake or acceleration button is clicked. This is because that is how the component is expected to behave, it will be updated to reflect the new speed.

Adding memoization in this component or even simply wrapping it in React.memo would therefore be completely unnecessary, and it is usually left to the good judgement of the developer to define the scope for preventing expensive renders.

We could have an entire separate blog on why not to use memoization everywhere. This is one such (amazing) blog.

A common real world use case could be a child component having an API call being fired on its initial render, with the method to make the API call being passed in from the Parent component.

I will leave this as an exercise for the reader, you can use the APIs provided by JSON placeholder for this.

Create a method to fetch some data in your App.js component, and pass this method as a prop to a child component. Create another child component to update the state of App.js, and you will see the API firing on every unrelated re-render.

If not memoized, the call would happen on every update of App.js by any sibling component. Which would indeed be troublesome!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Arun Kant Pant

Building things, breaking things, learning things. Frontend Engineer at The Economist Intelligence Unit.