React Memo - Rendering Optimization in React
In this post you will learn what is React.memo
and when you need to use it.
So let's look on the real example. Here I have a slider which is just a component inside React.
const App = () => {
const slides = [
{ url: "http://localhost:3000/image-1.jpg", title: "beach" },
{ url: "http://localhost:3000/image-2.jpg", title: "boat" },
{ url: "http://localhost:3000/image-3.jpg", title: "forest" },
{ url: "http://localhost:3000/image-4.jpg", title: "city" },
{ url: "http://localhost:3000/image-5.jpg", title: "italy" },
];
const containerStyles = {
width: "500px",
height: "280px",
margin: "0 auto",
};
return (
<div>
<h1>Hello monsterlessons</h1>
<div style={containerStyles}>
<ImageSlider slides={slides} parentWidth={500} />
</div>
</div>
);
};
And here is a code of this slider. We pass inside ImageSlider
a list of slides and inside we implemented the whole slider.
Searching for the problem
Inside this slider we are using useEffect
with setTimeout
to switch between slides. Every 2 seconds we jump to the next slide. It all works fine and you won't see the problem until some point.
useEffect(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
goToNext();
}, 2000);
return () => clearTimeout(timerRef.current);
}, [goToNext])
Now I want to add console.log
in useEffect
and inside our ImageSlider
component. After this I want to add a button which changes the state inside our parent.
const App = () => {
const [counter, setCounter] = useState(0);
...
return (
<div>
<button onClick={() => setCounter(counter + 1)}>Counter</button>
<div style={containerStyles}>
<ImageSlider slides={slides} parentWidth={500} />
</div>
</div>
);
};
This is just a counter which we increase but you will see what happens when we change the state inside our parent.
If we don't do anything we see console.log
and rendering of the slider every 2 seconds. But what is not fine is when we are clicking on our counter it rerenders our ImageSlider
This is the default behaviour inside React. When parent renders it rerenders all children.
Typically it's not a problem and you won't see that your child component way rerendered. But in our case our slider stop sliding every render. Which means while we click on button faster than 2 seconds it will never slide which is a bug.
Bug fixings
What is the problem here? Essentially our ImageSlider
is completely isolated and we don't care about any changes inside our parent except of changes in slides
and parentWidth
.
When you have a case that you want to avoid child rendering you must use React.memo.
In order to do that we must wrap the export of our ImageSlider
which memo
.
export default memo(ImageSlider);
What does it do?
So what should it do? If our props of ImageSlider
didn't change then our child component won't be rerendered. In some cases it can fix performance problems and in our case we will fix our slider problem.
If we check in browser we can see that it didn't help at all. Why it didn't work? By default React.memo
compares all props just with triple equality. Which actually means it is totally fine for the number like parentWidth
but not for slides
because it's an array.
To prove that we have a problem just in slides I can move all slides just for testing inside our component so we don't have an array in props.
const ImageSlider = ({ parentWidth }) => {
const slides = [
{ url: "http://localhost:3000/image-1.jpg", title: "beach" },
{ url: "http://localhost:3000/image-2.jpg", title: "boat" },
{ url: "http://localhost:3000/image-3.jpg", title: "forest" },
{ url: "http://localhost:3000/image-4.jpg", title: "city" },
{ url: "http://localhost:3000/image-5.jpg", title: "italy" },
];
}
As you can see now it doesn't matter how many times we clicked on the button - our ImageSlider
is not rerendered. We just get rendering every 2 seconds like it should be.
Which actually means that the problem is in our slides and React.memo
works just fine.
Correct comparison
How can we fix this problem? The first fix that we can do here is just move slides to the state property. It won't be recreated every time and we won't get into this problem.
const [slides] = useState([
{ url: "http://localhost:3000/image-1.jpg", title: "beach" },
{ url: "http://localhost:3000/image-2.jpg", title: "boat" },
{ url: "http://localhost:3000/image-3.jpg", title: "forest" },
{ url: "http://localhost:3000/image-4.jpg", title: "city" },
{ url: "http://localhost:3000/image-5.jpg", title: "italy" },
]);
As you can see in browser it works just fine.
Another solution here would be to write a custom comparator for the props inside our ImageSlider
.
const comparator = (prevProps, nextProps) => {
if (prevProps.parentWidth !== nextProps.parentWidth) {
return false;
}
return prevProps.slides.every(
(el, index) => el.url === nextProps.slides[index].url
);
};
export default memo(ImageSlider, comparator);
Here we provided to memo
a second argument which is a function and it compares previous and next props. If it returns true
our component will render.
First we compare parentWidth
and it is easy because it is just a number. Secondly we compare slides. Here we check that every single slide has the same url because it is our unique identifier.
If both props are same then our component is not rendered.
As you can see in browser now the problem is fixed and our component doesn't have unnecessary renders.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done