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.