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…
- The individual movie items are a lot more expensive to render and contain a lot of state
- 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.