Performantly Render a Large List of Items with React Context

Dave Ceddia
InstructorDave Ceddia

Share this video with your friends

Send Tweet
Published 3 years ago
Updated 3 years ago

One tripping point with Context is that it will automatically re-render the subtree under a Provider whenever that Provider’s value changes, which can lead to performance problems if you aren’t careful. In this lesson you’ll learn what to watch out for, and a simple trick for avoiding unnecessary re-renders to keep your app running smoothly. You'll also learn how to use React.memo to optimize individual function components.

Instructor: [00:00] Here, in the email provider, we are fetching new emails every two seconds. You can see the notifications coming in. In the notification context, we're cleaning up the messages every second. This cleanup function will remove messages that are more than three seconds old.

[00:16] We're rendering over a thousand emails here, and the app is a getting a little bit sluggish, because we haven't done any sort of performance tuning. If we open up the React dev tools, we can click on this gear, and turn on this highlight updates option.

[00:29] Now, every time the app re-renders, we see all the components flash with blue or green outlines. This means that every component is re-rendering. The first thing we can do to improve this is to open up the messageList file, which is where we're rendering the list of emails and the email component.

[00:44] Since this email component only depends on props, it shouldn't need to re-render the existing emails. We can wrap this function component in react.memo, which functions very much like React pure component, and will only re-render this component when its props change.

[00:59] Now, if we save, when the app refreshes, we can see that we are still re-rendering the emails every time. That's because one of these props is actually changing. This onClick prop is a new function every single time this list is rendered.

[01:13] We can improve this by passing down the callback function unchanged. Since it needs this email argument, and the email component is already receiving that argument, we can change the onClick to just pass the email to onClick.

[01:28] Now, when the app re-renders, you can see that we aren't getting the blue boxes around the individual emails anymore. This is a big improvement. If we go over to the React profiler, we can do a quick recording.

[01:40] We can see that a lot is going on every render here. The next thing we might try is to go up to messageList and wrap this in react.memo. This won't work, because messageList isn't receiving any props. There's nothing for react.memo to watch out.

[01:55] The reason messageList is re-rendering is because it's using two contexts here. Whenever one of these context values changes, this component will re-render. Now, need to go and optimize the contexts. If we open up the index file, we can see that we're using three context providers in our app.

[02:13] Let's tackle these one-by-one. First, we'll go over notificationContext, and we'll see where we're calling the provider, and we're passing this value, we're passing a brand new object every single time this re-renders.

[02:25] This brand new object will cause the consumers to re-render. We can fix this by combining all of the callbacks into this.state, and then just passing the state object. To do that, we'll implement the constructor, which takes props, and calls super props.

[02:40] Then move the state initialization into the constructor, and add the notify property, which will be the callback. Now, down at the provider, since notify is already part of state, we can just pass this.state. We can go and do the same thing to emailContext, where we write a constructor, it takes props, calls super props, and then move state initialization into the constructor.

[03:05] If we look down to the provider, we need this onSelectEmail property. We can take this, and move it into state. Now, in the provider, we can just pass this.state. The last context is userContext. Here, we can do the same thing.

[03:21] In the constructor, call super props, and initialize the state here. This provider is passing a user onLogin, and onLogout. These bottom these are easy enough to move into state, but this user value, which is set to state currentUser, doesn't match the name that we have in state.

[03:41] We'll change currentUser to just user. Now, we can pass this.state into the value. Now, in our app, you can see every second when the notifications are cleared, there isn't that much re-rendering. When new emails arrive, there's more that re-renders.

[03:58] If we go into the profiler, we can record another snapshot, and see that each render is now rendering many fewer components. The render durations are much shorter as well.

Ilham Wahabi
Ilham Wahabi
~ 3 years ago

great course!

Yazid
Yazid
~ 3 years ago

great course thank you, I just have one question, instead of putting all the context values inside the state why not using memoization to construct the context values

const m = memoize((state, notify) => ({ ...state, notify }))
<Provider value={m(this.state, this.notify)} >...
Carlos
Carlos
~ 3 years ago

Great course!!

Just one thing to talk about: setting functions (behavior) in the state doesn't seem quite correct to me (maybe I'm the only one) but Yazid's alternative sounds like a really good approach.

Thanks.

Dave Ceddia
Dave Ceddiainstructor
~ 3 years ago

Putting the functions in state is actually what the official docs recommend (see here) but I really like Yazid's idea and I think that would work. You could even use the new useMemo hook and avoid having to import a library or write your own memoize.

Lukas Pospisil
Lukas Pospisil
~ 3 years ago

Question please Do I have to put the state inside the constructor to improve performance, isn't enough to keep it without constructore??

Dave Ceddia
Dave Ceddiainstructor
~ 3 years ago

Lukas - you can initialize state outside the constructor, and that's what I normally do too, but if you do that you have to make sure that any of the arrow function-style class methods that you want to put into state are defined before the state = { ... } line. To me, it looks a bit weird to have state initialized anywhere other than right at the top, so that's why I put it in the constructor for this lesson.

Lukas Pospisil
Lukas Pospisil
~ 3 years ago

Thanks for answer, the course is awesome!

Dave Ceddia
Dave Ceddiainstructor
~ 3 years ago

FYI: The 'before' code for this lesson is here if you need it.

Raquel
Raquel
~ 3 years ago

Awesome course! I really love it!

Herbert Pimentel
Herbert Pimentel
~ 3 years ago

Thanks for this amazing content.

Hakan Karaduman
Hakan Karaduman
~ 3 years ago

That was great optimization tips, thanks so much for it.

Jaini Guevara
Jaini Guevara
~ 3 years ago

Thank you, quick and informative content!

Philip
Philip
~ 3 years ago

I love your vscode theme, What are you using?

Dave Ceddia
Dave Ceddiainstructor
~ 3 years ago

@Philip - the theme is Sarah Drasner's Night Owl