React Performance, v2

Memoization with useCallback

Steve Kinney
Temporal
React Performance, v2

Lesson Description

The "Memoization with useCallback" Lesson is part of the full, React Performance, v2 course featured in this preview video. Here's what you'd learn in this lesson:

Steve introduces memoization and explains how hooks like useMemo and useCallback optimize performance by avoiding unnecessary computations. He shows how to implement memoization in a component to prevent re-renders and improve application efficiency.

Preview

Transcript from the "Memoization with useCallback" Lesson

[00:00:00]
>> Steve Kinney: With that, we can start talking about this idea of memorizing stuff and making sure that if everything this thing relies on has not changed, then this doesn't need to change either, right? All right, so there are some slides for what we're going to talk about, but we are here and we are looking at it, so we're going to kind of do it in context, and then we'll kind of see it in another app in a second because there's a few interesting tasting notes here, right?

[00:00:29]
OK, we know we're defining these new and fresh every time. So let's play a game where we'll see, and we might have to do some stuff to the button component as well, but let's start conceptually here, and we will talk about kind of some of the memoization hooks that React gives us and a lot of their kind of tasty notes, edge cases, and quirks and abilities and stuff along those lines, and some stuff you kind of get for free, and some stuff maybe you know about, maybe you don't.

[00:01:00]
That seems ominous, but let's do it. So it would be nice to say that we needed to define these inside of our component because they do rely on at least set count as well as count. So they need to be defined in here, so we can't just define them outside the function so they don't get defined every time. But we can give React some hints that, hey, you don't need to compute this every time, and React's like, tell me how.

[00:01:32]
I will say this, if somebody's going to ask me, doesn't React compiler do this? The answer is yes. Ask me that at the very end so that we can segue into React compiler. You are right, if you were thinking about it, I'll do the kind of stump speech right now, which is React compiler is cool and it does a lot of this for you. However, there is a migration process into it. If you're working on an application, you probably have seen how long it probably took you to upgrade from React 17 to 18, 18 to 19, 16 to 17, what have you.

[00:02:08]
You know how long it takes every time you want to switch anything, and theoretically at this moment, React compiler is technically a release candidate. Now, Instagram.com uses it in production, which feels pretty safe, right? But there are some things where if you just do the worst crimes I have ever committed in a codebase was thinking I was very clever with find and replace, right? Some of these memoization techniques are performance, some of them are to prevent expensive operations that maybe React compiler won't fully know.

[00:02:49]
Some of them are for other purposes. Some of them are bad anti-patterns, but they are in the application, and the compiler will basically be like, yo, if I don't understand what's going on, I'm not doing anything. So like, understanding the concept is still important. Also, you might not be able to use these things immediately. It's kind of like many of the transitions in React, like going from class components to function components, going from not having an official context API to having one, six or seven other transition processes that we've all gone through, which is like new cool stuff drops.

[00:03:25]
It's kind of like working on browser features, you know, you gotta kind of wait a little bit. So I don't feel comfortable not teaching them just yet, but hopefully maybe one day. Same way I didn't bring up class components or any of that stuff just yet, and if I was to talk about TypeScript, I wouldn't even talk about prop types anymore, but we're not there yet with this one. So let's talk about the two memoization or one that we can talk about right now for locking stuff in place.

[00:03:55]
If you have used useEffect, which most of you have just from the questions I've gotten so far, the general vibe should make sense, which is we can basically say, hey, React, I'm going to tell you about what this thing relies on. If they have not changed, this function hasn't changed. We have two major memoization hooks: useMemo and useCallback, right? We're going to use useCallback now. UseMemo is to say, whatever you did to get this value for me, right, if none of the things that it relies on have changed, just give me the old value again, I'm, we're good.

[00:04:36]
UseCallback is saying, hey, this function that you made, that I'm going to use other places, if none of the things that it relies on have changed, then don't do the work to create a new one, keep the same one in memory. Not making a new function is both not making a new function, but also garbage collection is doing something, right? Having the old function that needs to get cleaned up is a thing that happens, right?

[00:05:04]
Again, immeasurable in this tiny application, but as it accumulates, a thing. So the nice part is it's somewhat easy. It's way easier to not, we can not, not useMemo. Wrong one. That was a test and you all failed. UseCallback and we can wrap it, and we kind of then give it the array of things it cares about. There are some nuances to this and some opinion. I don't care. This relies on technically on count and set count, right?

[00:05:41]
Pretty obvious. So the next time this component renders for any reason, it's going to check. Are count and set count the same values, right? And so count is like in this case, 0, so like, OK, still going to be 0 + 1. What else? And set count hasn't changed, and you'd be like, isn't that a function too? Give me a second. It won't create a new function. I'll use the same one it had last time. The interesting part is set count, all the setters from useState, which if you hover over the TypeScript annotation, you will really see is actually dispatch from useReducer.

[00:06:27]
UseState is just an abstraction over useReducer, right? And you will see, like in this case, it too is wrapped in useCallback and will always, like, set count will always be the same function. So it will not break this cache ever. Set function, you can call this component 1 trillion times, set count will always be the same, right? And then count theoretically could change, right? So some people will be like, you won't need to put set count in the array.

[00:07:04]
ESLint may or may not agree with you. I do, you kind of don't have to because this is the only one that should actually break the cache. And so we can kind of, so as long as count is the same, this function should be the same too. And so as long as count doesn't change, this shouldn't change. So we can kind of do that for both. We can do it. I mean, you'll see it for a second, we can do it here as well, right?

[00:07:39]
I gotta put the array in there. The interesting one is reset. If you realize the set count actually won't ever break the cache and that you can safely omit it, right, it really just depends on if you feel comfortable with it, but you can, arguably under the heuristic of not checking it and finding out the same as faster than checking it, but so infinitesimally tiny that I don't know. You'll notice this one doesn't really have any dependencies, right?

[00:08:18]
So in fact, this one will be not unlike set count, where basically, no matter what, it will effectively always be the same function, right? But you're like, hey, Steve, listen, for on increment and on decrement, if they're changing the count and count is the only thing that could change, why would I cache these, right? And that's fair, because like, effectively, this component takes no props. I need to change this.

[00:08:57]
It's driving me nuts. Sometimes ideas we have. If this component takes no props, right, means that honestly, the only time it would ever re-render, if because the application should have no props, it should have no state of its own by the time we, if we refactored all three, the only time it should ever re-render is if count changed, thereby you could argue to me that wrapping these in a memoization is bad, right?

[00:09:31]
And the rationale would be, I'm doing the work of checking when the only time the check would ever happen is when the check would break. And that is theoretically true, but with one nuance. There's a different API for your setters. Yes, they can take the new value, that's good if you have an input or something like that. They can also take a function, right? And the function gets a previous value and it's set to the return value of that previous function, which then if we notice the squiggly lines, means count's not involved, right?

[00:10:18]
And so, yes, the act of actually just wrapping the original one in a set count would technically make your app infinitesimally slower. Now again, there's the big things and then there's the accumulation of the little things, right? But then if we take this version and we use this other API for it where it's just kind of taking the previous value and letting you manipulate it, now, all three of these are never changing.

[00:10:51]
They are effectively as set in stone as set count, right? And they will always be the same value every time. So if we were to then go down and memoize the button to say, hey, if your props haven't changed from last time, right, then don't even try to figure out how to wire up this button, right? If we had not done that before, well, the only time that button's ever really re-rendering is when count changed, which means we would have broken the cache on these.

[00:11:24]
Now these are solid, right? They do still need to be in the function because they need to have useState in closure scope. But they have, just by a simple little tweak, now kind of stay referentially the same no matter what. And I think I gotta go put a useMemo on the button, but let's just try it out for a second. Oh, actually, in this case, right, like you can see that those buttons are no longer re-rendering because they are effectively the same.

[00:11:59]
And so, effectively, just the component re-renders because it has a new number, which totally makes sense. Again, as high as we need, as low as we can get away with, a little bit of caching in this case will go ahead and save us a lot of effort, right? And the bad side of this is that you need to do it, right? Now, what I would recommend usually is, especially if you find yourself doing this more than once, this is an opportunity to maybe make a useCounter, right?

[00:12:41]
That you can reuse and then like, yes, you've done all this kind of manual work, and then you never have to think about it again, right? So you could do something like const useCounter. We'll say initial equals 0. Let's just paste it all in there for a second and we'll say, let's actually call this something slightly better for a second. Decrement, decrement, decrement. Go away. Reset and then we could theoretically, oh, this is going to be fun, check this out.

[00:13:20]
We're going to be, it's going to be bad. It's going to be bad and then we'll fix it again, right? So then we're like, we're going to be a hero, we're going to go ahead and we're going to say that we've got the count, we're not going to export set count directly because we have all the other things for it. Increment, decrement, reset. And so now inside the component, we can do something like count, increment, decrement, reset equals useCounter.

[00:14:14]
I do need to change these real quick. And like that should have solved my problem, right? This one, it will. The thing you have to be careful of is that this is a new object every time. So if you were to pass the full object through, you will, it'll be a new object every single time. So that's where our other hook comes in. Now, because I've broken them out, and these are referentially the same thing, even though it's a new object, all the members are the same.

[00:14:53]
So if you were to pass that entire object, that's a new object every time. Everything in that object is the same every time, right? If you wanted the object itself to be the same every time, that's where that useMemo would come in and you're going to say, yo, if this is interesting, count, increment, decrement, and reset are all the same. Oh, it's gotta be a function. Then give me back the same object every time.

[00:15:26]
You do enter a new problem where count will change a lot. However, if something else changed and you were passing this all the way through, this itself will not trigger any of your renders. If count changes, the whole object changes, right? And we'll explore this again later, so I'm not going to perseverate over it too much. The answer would be to maybe return two objects, which is like the state and all the actions.

[00:00:00]
Now the actions, you can pass through as a bundle because they'll never change, and the count you keep separate. So sometimes the act is just splitting the stuff that changes from the stuff that doesn't change, and now you have more control, which almost no one does, and it's the easiest thing in the world. So now you can seem smarter than everyone.

Learn Straight from the Experts Who Shape the Modern Web

  • 250+
    In-depth Courses
  • Industry Leading Experts
  • 24
    Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now