Simulate PureComponent with a React Function Component

Elijah Manor
InstructorElijah Manor

Share this video with your friends

Send Tweet
Published 5 years ago
Updated 3 years ago

Historically, only Class Components could extend PureComponent or implement their own shouldComponentUpdate method to control when it's render was actually invoked. However, now you can use React.memo HOC to provide this same type of control with Function Components. This isn't specific to Hooks, but was released in React 16.6 and complements the hooks API.

Instructor: [00:00] Let's kick up a dev server for our little to-do app, where we'll be looking at performance tuning. On the right, you could see the app running. You could see that you could toggle items, delete them, switch the theme, etc. Let's open the browser's dev tools and look a little bit deeper.

[00:18] Already, we could see some stuff going on in the terminal. Let's clear that out to get a better look. Let's go up and start to add a new to-do item. Whoa. For each letter I type, we get a slew of logs from the to-do item. The item shouldn't be changing at all. Why is the code running? This is not good. Let's go investigate.

[00:39] Let's first open the to-do list and see what's going on. If we scroll down, we'll see a map of our array of to-dos that renders that to-do item component. Let's look at that.

[00:50] The to-do item currently is a React class component. You could see the console.log on line 8 that has been getting called way too many times. One solution could be to extend Pure.Component instead of Component.

[01:03] Let's run the code and find out. We'll clear out our logs so that we could see better what's going on. As we type, we still see tons of logs. It doesn't seem to be any better than before.

[01:17] Pure.Component isn't magical. It compares the previous props in a state to what is being provided, and if they're different, it'll run the render to see if things should change. If the props and state don't change, it knows to bail and not even try.

[01:30] In our case, if we look back at our to-do list component, we're actually creating new functional wrappers for our onChange and onDelete handlers, which from a shallow comparison standpoint, are different every time we render.

[01:44] To give ourselves more control, we could go back to extending Component and mainly provide shouldComponentUpdate to the class, and only check if the to-do has changed from the previous props versus the to-do getting passed to us.

[01:58] We only want to return true to tell the component to update if the to-do values are different. Otherwise, we don't want it to try at all.

[02:08] If we try running this again, you'll notice that the behavior is more like what we expected. The code inside the component is only running when it's actually being modified.

[02:19] How do we do something similar in a function component world? First, let's convert this component and find out. We'll need the useContext hook to make sure we could properly theme our component, then replace our class with a function.

[02:33] We'll go ahead and destructure the props we need right in our parameters, to-do, onChange, and onDelete. We can, for now, kill the shit component update. We don't need the render part anymore, either. Whatever is returned is what will be rendered. For our console.log, we'll provide our props that we destructured.

[02:52] Lastly, we need to handle the theme. We'll create a theme variable and set it to the useContext hook, passing our theme context. Then we'll pass our theme to the item and button components on lines 10 and 19. To clean up, we don't need the context type or export anymore, since we did that up on top. That should be it.

[03:15] If we test our code again, you'll see that things got worse, not better or the same. There's a feature that came out in React 16.6, which is currently released, called memo, which is a higher-order component that is similar to Pure.Component but is intended for function components instead of classes.

[03:35] It'll also shallowly compare props and only run the render if they have changed. To use it, we'll just wrap our function with a memo function, and we'll try this again.

[03:48] You may have already guessed this, that it didn't solve our problem, and you'd be right. It's the same problem that we had before. Our onChange and onDelete function wrappers keep changing, so the shallow compare keeps saying that we've made a change.

[04:02] Thankfully, the memo higher-order component takes a second parameter that allows us to provide a custom comparison function. Let's provide one.

[04:10] The function provides previous props and next props. If they represent the same, we're supposed to return true. We'll destructure the to-do item off of both props and call them prev-to-do and next-to-do and return if they are equal. This is it. This should work.

[04:30] If we come back to test, we'll get the behavior that we're looking for, but this time in a function component. Wouldn't it be nice if our pure component version worked, and we didn't have to provide that second argument to memo? Let's go ahead and remove that piece and solve this in a different way.

[04:49] As we mentioned before, the problem is that we keep creating this thin function wrapper around our dispatchers. We could solve this with yet another React hook. Outside of our return, let's create a handler for handleChange and set it to the same logic as below.

[05:08] Then we'll import the useCallback hook from React and wrap that around our handler implementation. useCallback will return a memoized version of our callback. The second parameter indicates when the memoized version should change.

[05:24] In our case, we want it to always be the same, so passing it an empty array conveys that message. Now, we'll just copy this and tweak it for our handleDelete, and swap that out in our onDelete.

[05:42] If we run our app yet again, we'll see that the performance we want is as it should be, but this time, we could use a pure component or the memo high-order component without the custom comparison function.

Viktor Soroka
Viktor Soroka
~ 5 years ago

Great lesson. Showing multiple options for solving the problem is definitely helpful.

David
David
~ 5 years ago

Awesome lesson! Thanks! I was wondering how would you use memo if you had to compare couple of values... I have this codesandbox https://codesandbox.io/s/p2pnwp6pom and a FieldContainer that should rerender if the name value changed and errors array has been updated on input blur... Cant figure this one out... Would appretiate your help! Thanks

Zhentian Wan
Zhentian Wan
~ 5 years ago

Great lesson! I still have few questions after watching the lesson,

  1. is that true, for function component, it is better to wrap with React.memo()? Because even after using useCallback in TodoList, If I removed React.memo from TodoItem, it still re-rendering every time I type something in NewTodo input.
  2. is that ture, for every callback function I use, I need to wrap with useCallback? Because I saw About component, also re-rendering every times I types, ... I have to add both React.memo to About and also use useCallback on onClose prop, then it gets better.

It gives me feelings that every times I should wrap function component with React.memo() and use useCallback whenever necessary... I am not sure whether that is true of not. Thanks for help :)

Elijah Manor
Elijah Manorinstructor
~ 5 years ago

Zhentian,

Great questions. I like the idea of using memo with the 2nd argument vs using memo without the 2nd argument and using useCallback.

As for your second question, If you want to bail out of the render phase you'll need to use memo with or without the 2nd parameter. memo (1 arg version) is very similar to PureComponent that you use with classes so you need to be careful that the props don't change... so you can useCallback to help with that. If you do have callbacks I'd rather use the 2nd argument of memo to just target the parts I care that are changing.

Some might consider this a pre-optimization and might wait until they see degradation in their app speed before going into such optimizations. Even if React runs your render, it'll compare the output with the Virtual DOM to see if any real changes are needed when it updates the DOM.

I hope that helps

Elijah Manor
Elijah Manorinstructor
~ 5 years ago

Viktor, thanks I'm glad you found the various solutions helpful! Often times there are many ways to do the same thing.

Elijah Manor
Elijah Manorinstructor
~ 5 years ago

David,

That's an interesting one. In the case of your FieldContainer the value that you are typing ends up populating the FieldContainer with a new value, so your memo 2nd argument is always different because it needs to update the Field={value}.