optimizing renders on massive lists with React.memo

A common task I use for react is rendering large datasets in the UI. For example, a large list of movies or books. Here’s a simple component that renders a list of movies.

import React from 'react';

const MovieList = ({ movies }) => {
  return (
    <ul>
      {movies.map((movie) => (
        <li key={movie.id}>
          <strong>{movie.name}</strong>: {movie.description}
        </li>
      ))}
    </ul>
  );
};

export default MovieList;

As long as you’re using a unique `key` attribute in this case, renders are pretty fast. In the example above, only simple list items are being rendered for each movie. It’s a small set of elements and there’s no complex state involved.

But what if…

  1. The individual movie items are a lot more expensive to render and contain a lot of state
  2. The state cannot be localized to the smaller child components and needs to live at the root level (because it’s shared with other components)

Here’s an example where we have a parent level state that maintains rating data and passes that down to render both the movie list and a sibling recommendations component.

import React from 'react';

const ExpensiveMovieItem = ({ movie, movieRating, setMovieRatings}) => {
   return (
     ... very expensive render ...
   );
}

const MovieList = ({ movies }) => {
  const [movieRatings, setMovieRatings] = useState({})
  return (
    <div>
      <ul>
        {movies.map((movie) => (
          <ExpensiveMovieItem movie={movie} movieRating={movieRatings[movie.id]} setMovieRatings={setMovieRatings}/>
        ))}
      </ul>
      <Recommendations movieRatings={movieRatings}/>
    </div>
  );
};

export default MovieList;

In this case, if setMovieRatings gets called in any of the children ExpensiveMovieItem components, the parent state will update and all of its children will re-render (even if the props for the majority of components in the list stays the same). One common misunderstanding I had for a long time is that when props stay the same, a component does not re-render. In reality, any time a parent UI component state changes as a result of setState, all of its descendants re-render.

If this is a large list (1000+) items, this re-render can create noticeable lag. In this case, if the re-render takes 200ms, it’ll take 200ms between when a rating for a movie is updated to when it’s actually reflected in the UI. Since React does not care to skip re-renders automatically based on props, it’s up to you to tell it when to skip a full re-render for a component.

React.memo

React.memo is a function that accepts a component (and an optional prop comparison function) and returns another component. This new component has special memoization behavior that skips re-render based on either the built-in or user provided prop check.

Going back to the original example, here’s how you turn a normal expensive component into a memoized one:

import React from 'react';

const MemoizedExpensiveMovieItem = React.memo(({ movie, movieRating, setMovieRatings}) => {
   return (
     ... very expensive render ...
   );
});

const MovieList = ({ movies }) => {
  const [movieRatings, setMovieRatings] = useState({})
  return (
    <div>
      <ul>
        {movies.map((movie) => (
          <ExpensiveMovieItem movie={movie} movieRating={movieRatings[movie.id]} setMovieRatings={setMovieRatings}/>
        ))}
      </ul>
      <Recommendations movieRatings={movieRatings}/>
    </div>
  );
};

export default MovieList;

Now if you update the parent state, only the children with changed props will render. This technique will work out of the box if all of your props are non-object primitives (strings, numbers), but you’ll have to be more careful if you have objects because the default comparison method is using Object.is, and it’s pretty common for the identity of objects to change across re-renders in React even if the literal values are the same. For example, if you’re re-creating functions that are being passed in as props then you’ll cause a re-render. Or if you’re doing object cloning in setState which creates new objects with the same values but different identities. You can get around these issues by either simplifying the prop params or providing a custom property checker.

Leave a Reply

Your email address will not be published. Required fields are marked *