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:
// App.jsimport React from 'react';// added some basic styling
const centerStyling = {
display: 'flex',
minHeight: '100vh',
alignItems: 'center',
justifyContent: 'center'
};function App() { return (
<div style={centerStyling}>
</div>
);
}export default App;
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.
function App() {
const [speed, setSpeed] = React.useState(0);
return (
<div style={centerStyling}>
</div>
);
}
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:
import React from 'react';const Engine = ({ handleAcceleration }) => {
return (
<div>
<button onClick={handleAcceleration}>Accelerate!</button>
</div>
)
};export default Engine;
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:
import React from 'react';
import Engine from './Engine';const centerStyling = {
display: 'flex',
minHeight: '100vh',
alignItems: 'center',
justifyContent: 'center'
};function App() {
const [speed, setSpeed] = React.useState(0);const handleAcceleration = () => {
setSpeed(speed => speed + 1);
};return (
<div style={centerStyling}>
<div>
// passing it as a prop
<Engine handleAcceleration={handleAcceleration} />
</div>
</div>
);
}
Similarly, we will also add a method to set the speed back to zero, which will act as the “braking” mechanism:
// App.js
import React from 'react';
import Engine from './Engine';const centerStyling = {
display: 'flex',
minHeight: '100vh',
alignItems: 'center',
justifyContent: 'center'
};function App() {
const [speed, setSpeed] = React.useState(0); const handleAcceleration = () => {
setSpeed(speed => speed + 1);
}; const handleBrake = () => {
setSpeed(0);
}; return (
<div style={centerStyling}>
<div>
<Engine handleAcceleration={handleAcceleration} />
</div>
</div>
);
}export default App;
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:
// Odometer.jsimport React from 'react';const Odometer = ({ speed }) => {
return (
<div>
<h1>{speed}</h1>
</div>
)
};export default Odometer;
Followed by the Brake.js file, also alongside App.js:
// Brake.jsimport React from 'react';const Brake = ({ handleBrake }) => {
return (
<div>
<button onClick={handleBrake}>Brake!</button>
</div>
)
};export default Brake;
Now all that is left to do is import these into App.js, and render them into the browser.
// App.jsimport React from 'react';
import Engine from './Engine';
import Brake from './Brake';
import Odometer from './Odometer';const centerStyling = {
display: 'flex',
minHeight: '100vh',
alignItems: 'center',
justifyContent: 'center'
};function App() {
const [speed, setSpeed] = React.useState(0); const handleAcceleration = () => {
setSpeed(speed => speed + 1);
}; const handleBrake = () => {
setSpeed(0);
}; return (
<div style={centerStyling}>
<Odometer speed={speed} />
<div>
<Engine handleAcceleration={handleAcceleration} />
<Brake handleBrake={handleBrake} />
</div>
</div>
);
}export default App;
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:
With that, we can check to see if everything is working as intended.
Great, now we can look at all the problems associated with this “Car”.
Child component re-renders
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:
const Brake = ({ handleBrake }) => {
return (
<div>
<button onClick={handleBrake}>Brake!</button>
{console.log('called from break')}
</div>
)
};
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:
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.
useCallback with React.memo
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:
import React from 'react';const Brake = ({ handleBrake }) => {
return (
<div>
<button onClick={handleBrake}>Brake!</button>
{console.log('called from break')}
</div>
)
};export default React.memo(Brake);
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:
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:
const handleBrake = () => {
setSpeed(0);
};
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:
function App() {
const [speed, setSpeed] = React.useState(0); const handleAcceleration = () => {
setSpeed(speed => speed + 1);
}; const handleBrake = React.useCallback(() => {
setSpeed(0);
}, []); return (
<div style={centerStyling}>
<Odometer speed={speed} />
<div>
<Engine handleAcceleration={handleAcceleration} />
<Brake handleBrake={handleBrake} />
</div>
</div>
);
}
Now if we have a look at our application, we will see that the Brake component is only rendered in the initial render cycle:
const handleBrake = React.useCallback(() => {
setSpeed(0);
}, [ ]);
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
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:
function App() {
const [speed, setSpeed] = React.useState(0); const handleAcceleration = () => {
setSpeed(speed => speed + 1);
}; const handleBrake = React.useMemo(() => () => {
setSpeed(0);
}, []); return (
<div style={centerStyling}>
<Odometer speed={speed} />
<div>
<Engine handleAcceleration={handleAcceleration} />
<Brake handleBrake={handleBrake} />
</div>
</div>
);
}
Which will give us the same result. When seen side by side:
const handleBrake = React.useCallback(() => {
setSpeed(0);
}, []);const handleBrake = React.useMemo(() => () => {
setSpeed(0);
}, []);
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.
Memoizing simple props with just React.memo
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:
import React from 'react';const Limits = ({max, min}) => {
return(
<div>
{console.log('Called from Limits')}
<div>
Max: {max}
</div>
<div>
Min: {min}
</div>
</div>
);
};export default Limits;
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:
import React from 'react';
import Engine from './Engine';
import Brake from './Brake';
import Odometer from './Odometer';
import Limits from './Limits';const centerStyling = {
display: 'flex',
minHeight: '100vh',
alignItems: 'center',
justifyContent: 'center'
};const limits = {
max: 80,
min: 40
};function App() {
const [speed, setSpeed] = React.useState(0);const handleAcceleration = () => {
setSpeed(speed => speed + 1);
};const handleBrake = React.useMemo(() => () => {
setSpeed(0);
}, []);return (
<div style={centerStyling}>
<Odometer speed={speed} />
<div>
<Engine handleAcceleration={handleAcceleration} />
<Brake handleBrake={handleBrake} />
</div>
<div>
<Limits max={limits.max} min={limits.min} />
</div>
</div>
);
}export default App;
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:
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:
...export default React.memo(Limits);
And now if you open the browser, then after the initial render, you will not see the console log appearing in your browser:
When not to use memoization
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.
Real world use case (an exercise for you)
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!