Mastering React's useEffect

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
Of all the React hooks useEffect is the one that I find that people struggle with the most. It doesn't fire. It fires too often or it fires when you don't expect it. So on this Blue Collar Coder video, we are going to dive in deep on useEffect. I'm Jack Herrington, a Principal Fullstack Software Engineer, and I'm going to give you a set of rules to live by so that you can tame useEffect and get it working on your side. Let's jump right into it. So the first thing going to do is create a React App. We'll call it taming-use-effect. And then I'll bring that up in VS code. Okay, Now that I've got that going, let's go into that App.js file and I'm just going to replace this with something really small. This basically I div with hello in it and then in the App.css. Yes. I'm also going to replace that with something that's just zoomed up so we can see it and padded. All right. Let's start this. Okay. So that looks like a pretty decent starting point. Now, we're actually going to be using some fetches in here, and that's how we're going to primarily try out useEffect. So I'm going to go create some data over in the public directory. And the first one will be called jack.json. It's going to have my name. And then we'll also have in that same directory sally.json with a name of Sally. So in the first part of this video, we're going to create our own basic version of react-query. I'll call it useFetch, and it'll just be a custom hook that does a fetch for us. So I'm going to create a new file called useFetch.js. And in there I'm going to import React as well as useState, because we're gonna need somewhere to hold that state. I'll bring those in from React and then I will export a useFetch function which takes some options. At the moment we're just going to go and set the data. So we'll set the data to null and then I'm going to return an object with that data. And now I'm going to split screen this because I’ll be primarily working across these two files for this whole demonstration. So over here in our app, the first thing going to do is put in a console.log so we can say app rendering so that we know when this is rendering. And then I'm going to bring in our useFetch from our local file. And then up at the top here, I guess by convention we're just going to call, useFetch and I'm going to give it a URL with my name. So jack.json. And then down here I'm just going to go in JSON.stringify that. So I’m just gonna I copy and paste that and then replace the hello here with JSON.stringify. With the data. Okay, so what do we expect? Well, currently data is null, right? So we expect null over there. I was going to see if we get that on the screen and we do. Let's go check our other revision, which is over in the console and we see that our app is rendering. Fantastic. Good stuff. So let's get to the good part. Let's go bring in useEffect from react over in useFetch and then with that useEffect, I'm going to call fetch, I'm going to take that options URL that we got given. I'm going to take the JSON from that and then I'm going to set that data from the payload that I got back. All right, So let's go take a look. All right. Well, we can see is already we're having some problems. Our app is rendering eight on over and over and over again like this. That's not good. So what we can do to start off is put a console.log inside of our useEffect and see if that's the culprit. So this is our useFetch, useEffect. All right, let's go take a look. And yes, we can see that we're getting app renders and use effects over and over and over again. So let's jump into the code and see why that is. So React functional components are just basic functions, right? And so they always go from the top to the bottom until they hit return. So in this case, the first thing we going to do is call our useFetch. That's going to come in here and it's going to get our data that's just going to give it back to us the way we saw. Then we're going to hit that useEffect and we're going to go to see what the dependency array is. And there's no dependency array specified here. So in that case, it is going to then asynchronously say, Cool, I need to run that function. It's then going to return the data which is currently null. That's going to come back here, go to that console.log, then we're going to return these elements. And then what happens is this useEffect fires and we get that set data that specifies that we get some new data and that forces a re-render on this component. And we go all the way back through the same thing again. And each time we're hitting that useEffect and we're looking at that dependency array and we're saying; you got to go and redo it. So that's why we're getting this infinite loop of useEffects. So how do we fix this? Well, we put in an empty array for the dependency array and lets refresh, and there we go. We get it once we get our data just the way that we want it. But we want to get different data sometimes. Sometimes we want to get jack.json. Sometimes we want to get sally.json and we want to be able to do that in a SPA way. So we want to be able to reuse this useFetch. So I'm going to create some data to store the URL that we currently want, all that URL, and I'll just set the URL here and then I'm going to go bring in useState from React and then I'm going to bring in some buttons that are below this that when you click them, it will set that URL to jack.json and sally.json. So let's go see how we're doing. All right, So we've got our buttons, but we're also getting this uncaught error in promise, unexpected token. So why is that happening? Well, if we go over here to the network tab, we can see that we're making a really weird request to null. So that's not good. So why is that? What if we go over into our useFetch? We can see that we're not actually checking to make sure that option's URL is non-null. So let's put in a check for that. Okay, it looks good. Let's try it again. And now it works. But if I click these buttons, we get an app re rendering because we're changing that state, which is forcing the app to re render. We're not actually getting any change the data. So let's go take a look over in our useFetch code and see there's a way we can do that. Well we're not actually rerunning that useEffect based on changed options. So what I'm going to do is just because I know that we're going to be using a lot of different options, I'm just going to put all of options in here. So I'm going to put it save and we'll see what we got. There is going to hit refresh and now looks okay, but when I hit Jack, now we get into this infinite loop. So why is this happening? Well, it comes down to JavaScript references and understanding JavaScript references and how they work is critically important to understanding how useEffect and useCallback and useMemo works. Since they all use dependency arrays and dependency res work on the evaluation of scalars and references. So let me jump over to Quokka and talk about what I mean here. But in the meantime I'm going to go and take that back down to an empty array because otherwise it's going to crash my Chrome tab because it's just going to keep cycling like that, which is crazy. All right. So this is a Quokka tab. It's powered by the Quokka extension, which is free. And if you paste code into it, then it automatically evaluates for you. So let's take the basics here, right? True equals true. We know that. That's true. False equals true. We know that that's false. Let's take another example of a number. This time does ten equal ten. Yes it does. Does say 12 equal 10. Of course it doesn't. How about strings? Does Jane equal Jane? Yes, it does. Now, if I make a variable, can I then compare that to that? Can I say person equals Jane, does that work? Yes, that's true. Okay, cool. So these are all scalar booleans, numbers, strings, these are all scalars and they are evaluated and compared by value. All right, now let's try something else is try comparing two objects together. So I've got an object here where A is one. And I’m going to compare it to another object where A is also one and I get false. And the reason is because I'm comparing the references to these two objects. This is a different object than this object, even though the values of those two objects are the same. Now, if I create a new object with a as one there and then I say, is that equal to object? That's true, because I am looking at the same object in both cases. Can I also compare to a1 as a different object? And it again, it's false. All right. Now the same thing applies to an array. So if I take one empty array compare it to a different empty array, they are not the same because they're comparing, again, the references of those two arrays and not the contents of the two arrays. Reinforce this a little bit by saying, does one equal two? If I do one and one again, still false, even though the contents of those two are the same. All right. Now you might be saying, whoa, hold up, buddy, because you're telling me that these two arrays compare to false, they're not the same. And useMemo and useCallback and useEffect all use dependency arrays and they compare the two arrays. And you're telling me that even if the contents of the arrays are the same, they compare to false and false means that we're going to rerun the useEffect or the useCallback or the useMemo. That's absolutely true. It does mean that false does mean that those are going to be rerun, but that's not the way that does the comparison. It actually does a comparison of each item. So I've created a little function here called depCompare. And it basically does what React does. You have your old dependencies and your new dependencies. The first thing he's going to do is check to make sure that the length of the dependencies is the same between those two and otherwise is going to turn false. And then it's going to look to see if every single item in the old dependencies equals the new dependencies and it's going to use that same triple equals. So it actually doesn't compare the array reference of the dependency array itself. It compares the, the reference and the value of each of the items in the dependency array. So in this case, if we run on two empty arrays like this, then it compares to true because the length of those are the same and every item in those is the same. There's no items that makes it easy. Now how about ones like this? So an empty array versus one that has one in it that's false. One and one. That's true. That's okay. How about false in one again? That's false. How about two person names? So you've got Jane and you've got Jane. That's fine. Those compare. The whole thing's true. All right, so how about object references? So we've got the original object as well as also the original object. Now, as compared to true. Yes. Because, again, the references of those two objects are the same. All right. Now, let's get a little tricky here and take a reference copy of object and then compare those two. Are those are the same? Why? Yes, they are. Because this object reference copy is just the exact same reference as object. And then finally, let's take a look at comparing object, which has a one to another object, which also has a one. And that's going to be false because again, the reference is is what's important when it comes to arrays, objects and functions. So the big learning in all this is that when it comes to primitive values like booleans and numbers and strings, when those going to dependency arrays, those are really fine, easy to work with. You're going to be comparing by value and not by reference. But when you have objects or arrays or functions in your dependency array, then you're potentially going to have problems. And let's go jump back in the code and see why that's happening here. All right. Let me bring back my options and we'll see that we're in this issue of these never ending runs. That's not great. Okay. So let's go back over to our code and see what the problem here is. All right. So again, we go through the code from top to bottom like this, right? And we start off getting that state. That's fine. We then create a new object and we pass it on to useFetch. useFetch gets its data down here and then looks at that and we know that we have a new object now. And a new object is not going to match the old object because it's done by reference and not by value. And so that's the problem here. Every time it goes through this, even though the contents of this object are the same every single time, it's a new reference to a new object. And that's why this use effect is triggering over and over and over again. So let's talk about some ways to fix that. So what we want to do is pass the same object. So one way to do that would be to say, okay, we're going to have my options up here and it's going to have a URL and it's me, jack.json and we'll just pass that. All right. Let's try that out and hit refresh. And it looks good. Now, if I had these buttons, we do get some app rendering, but we don't actually get a useFetch or useEffect because we're always passing in the same myOptions over and over and over again and preferentially those are the same. And so we don't trigger the useEffect. But of course now we don't have access to that URL, so that's a problem. So what's a way to fix that? All right. So another way to fix that would be to use useMemo. Now I'm going to use useMemo to create myOptions and have useMemo return an object with URL. And every time URL changes, then it's going to go and create a new myOptions. Now let's go send that off to useFetch and see how that works. All right, let's refresh. And that works just fine. And we only get one rendering cycle each time. Why? Because useMemo is actually doing the work of creating that single solitary object and referentially every time you call useMemo, you're going to get back exactly the same object. But of course this is a drag nobody wants to go and use useMemo on these options like this one. What I really just want to do is use URL just like I was doing before and make that work. So how to do that? Well, let's go over here to options and all we really need to do is just say, Well, the only thing we depend on here is options.url. So I'm just going to add that in as that. And now when I hit refresh, that works just fine. So what is the salient learning here? You want to constrain your dependency array to just the values that you're using, and you want to make sure that the types on those dependency arrays like this one here are primitive values like strings, numbers and booleans. So we want to make our useFetch a bit more interesting. We're going to have an onSuccess callback that is optional. I'm going to say options.onSuccess and it's optional. So I'm going to put in question mark dot there. And so that's only going to get invoked if there is an option.onSuccess. Now, knowing what I know before about making sure that all of the items that we're using inside the use effect are referenced in the dependency array, I'm going to go and add options.onSuccess. And there we go. Let's try this out and it looks good. But of course, I want to see if onSuccess works. So let's go and add in an onSuccess. And I'm just going to have it do a console.log saying success. All right, it looks pretty good. Let's refresh and hit Jack and then immediately we're back into our cycle. All right. So why is that? Well, if we look back through this function, every time we are running App, we are creating a new function. Even though the implementation of this doesn't change, we're still creating a new function with a new reference every time. And that's doing exactly what happened with the options. But this time we're just looking at a function. So what we need to do is not depend on this function, pull that out. But of course we want to be able to go and change onSuccess later on. We want to have that kind of symmetric behavior. So how do we do that? So one trick that folks use is to use a reference. So we do use ref and then we create a reference locally. We'll call it savedOnSuccess. And then we use useRef ref and give it the options.onSuccess. And then in here, instead of calling options.onSuccess, we call current on that. Let's take a look see if it works. That works perfectly every time. Okay, cool. But of course we want to be able to change this and useRef works like useState. You give it an initial state, but that won't actually change it later on. So another thing, if you will do is they couple this with useLayoutEffect and useLayoutEffect is just like useEffect, except that it's synchronous. So we'll create a function here. We'll have it depend on that options.onSuccess. And then inside of this we'll just set that current value to whatever the new options. onSuccess. And now any time that options.onSuccess changes, this current will be updated and we'll use the most current value in here regardless of the fact that this might already be in flight, which is really cool. So let's save it. I'll make sure that it still works. It does. Awesome. Now another thing that folks do is create a helper function that makes this actually a lot easier. I'll call mine useCallbackRef and what we'll do is return the saved onSuccess. But we're just going to call this callback ref. We’re going to change our options.onSuccess to callback and now I can just use useCallbackRef as opposed to useRef. And that works right too. And of course all of this code is available to you and GitHub. You don't have to write that for yourself. Let's go over there and grab it’s in the description down below. Okay, So a quick check in. What strategies have we learned so far when it comes to useEffect and how to tame it? Well, these are two strategies that you can use, both as you're writing code and as you are evaluating code. When you're looking at pull requests, the first one is always specify a dependency array, even if it's empty. The second one is look at each and every item in the dependency array. If it is a string, a number or a boolean a primitive value, it's probably okay. If it is a function or an object or an array. Then that's something you're going to need to make sure that you do something like this callbackRef or something like that to make sure that you're doing that the right way. All right. Let's take a look at two more gotchas when it comes to useEffect that I often see people run into and the first one is around cleanup functions. So let's take a look at use effect in particular. And we know that in this we are not actually returning a cleanup function. So a useEffect can optionally have a cleanup function that comes after it. And when the values change, the original cleanup function is called before the new useEffect function is called. So we haven't returned one here. What I think we should do is we should decide whether or not we've been cancelled. So I'm going to create a let value here and say isCanceled and we’ll set it to false. And then in the cleanup function, we're going to say that we have been canceled and what this means is should the component that actually using this useFetch get un-mounted or should this options URL change, we are going to cancel the current fetch if it hasn't already completed. So the way that we can do that is just to simply check in here. If we are not canceled, then send along the data. But if we are cancelled, then don’t. And so what this means is if you have jack.json as a request and it's taken one long time and then you go request sally.json and that returns first, you don't get the canceled jack.json coming in later because it took longer to run. So it's a good way to make sure that you don't get those kind of race conditions and out of sequence useEffects. Now the second thing that I see people run into a lot is referencing state inside of a useEffect that alters that state. So to look at this. I'm going to actually have to create a different example. So back in App.js I'm going to create a new hook. I'm going to call this one useStopwatch and it's going to have a count. And so for every second that the stopwatch runs, it's going to add one to that count and it's going to return the count down here and we're going to have a useEffect and we'll start off with an empty dependency array. And I’ll bring in useEffect. So in this useEffect, I'm going to create an interval by using setInterval. And now that takes a function and I'm going to do set count to count plus one and put it on a timer. A thousand and then a return a callback function that clears that interval. So far so good. So let's take a look in here and let's try this out. And I'm going to put down here after the Hello count is count. Alright let’s go take a look. Now goes from 0 to 1 and then it stops. So what's happening here? Well, can we go and see why that is? Well, if we go in here, we'll make sure that we're actually getting called. So let's put a console.log in here with the count. And now I can see they're running, but the count is always zero. And why is that? Well, what's happening here is that when useEffect is run, we're actually capturing the value of count at that point where count is zero. And then that value never changes. So what can we do? We can put the count value into the dependency, right? It's a number. That should be fine. Let's see how that goes. Let’s run and now it works. Well, what you find is that you're actually running this effect over and over and over again. So let's do this. Let's take a look and call this useStopwatch useEffct. And we can see that we're actually rerunning this useStopwatch useEffect every time, because this count is changing. That's calling this cleanup function and then creating a new interval. And that's not exactly what we want. We want the original interval to just keep going over and over and over again. So we need to remove this from the dependency array. But that's a problem because we know when we do that we have a stale value for count. So when you run into problems like this, know that there's two different ways to call a state setter. You can call it with a single value like we have here, or you can call it with a function. And that function takes a previous value and then you get to give it the new value. So in this case, we're going to say previous to previous plus one. Now if I hit save and refresh, we see that the useStopwatch useEffect only gets run once count still remains at zero, but the value keeps going up because we no longer depend on count. So this functional variant of the states setter is a safe and sane way to mutate the state in the same effect that actually looks at that state. Well, I hope you've learned a lot about dependency arrays and useEffect and how to make use of some strategies that help tame useEffect so that it works for you and not against you. Of course, if there's anything that you've run into that I haven't mentioned here, be sure to put that in the comments section down below. I'd love to hear about it. Meantime, of course, hit that like button. If you like the video, hit the subscribe button, if you really like the video, and you'll be notified the next time a new Blue Collar Coder comes out.
Info
Channel: Jack Herrington
Views: 141,293
Rating: undefined out of 5
Keywords: blue collar coder, jack herrington useEffect, React useEffect, useEffect Reactjs, useeffect dependency array, Function Dependencies in useEffect, Dependency Array in useEffect, useEffect cleanup functions, Cleanup functions useEffect, React.useEffect, useEffect hook, jack herrington, useEffect, cleanup function useeffect, reactjs cleanup functions, react useEffect, mastering useEffect hook, mastering react useEffect, mastering useEffect, React js, react useeffect
Id: dH6i3GurZW8
Channel Id: undefined
Length: 25min 19sec (1519 seconds)
Published: Mon Jan 24 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.