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.

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
Did you like my post? Share it with friends!
Don't miss a thing!
Follow me on Youtube, Twitter or Instagram.
Oleksandr Kocherhin
Oleksandr Kocherhin is a full-stack developer with a passion for learning and sharing knowledge on Monsterlessons Academy and on his YouTube channel. With around 15 years of programming experience and nearly 9 years of teaching, he has a deep understanding of both disciplines. He believes in learning by doing, a philosophy that is reflected in every course he teaches. He loves exploring new web and mobile technologies, and his courses are designed to give students an edge in the fast-moving tech industry.