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.