React Memo - Rendering Optimization in React
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.

Initial project

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.

Problem

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" },
  ];
}

Fixed problem

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.

Fixed problem

As you can see in browser now the problem is fixed and our component doesn't have unnecessary renders.

And actually if you want to improve your Javascript knowledge and prepare for the interview I highly recommend you to check my course Javascript Interview Questions.

📚 Source code of what we've done