Photo by RetroSupply on Unsplash
Day 36-Effects and data fetching in React-Lesson 7
Hi, welcome to day 36 of my 100 Days of Code challenge.πͺπ
Table of contents
Cleaning up the title
So, a cleanup
function, as you recall, is simply a function that we return from an effect.
A cleanup
function is just a small function we add to our code. It's like a janitor that comes in after a party to clean up π€.
Remember, we need a cleanup
function whenever the side effect introduced in the effect keeps happening after the component has already been unmounted.
When the Movie
component is mounted ππ, here the document.title
is set to the movie title, which is exactly what we want.
But now, although we no longer have the Beekeeper
movie on the page, the side effect is still occurring, and the title continues to display the old movie title we had selected before. π
ππThe cleanup
function is the perfect solution for this scenario. In this case, we want it to fix our page title back to normal.
So, since a cleanup
function is simply a function that we return from an effect.
All we need to do in this function is to reset the document.title
to its original form. Let's name it MovieMania
, and that's all we have to do.ππ
And as we go back now or after a movie has been added to the watch list, the title reverts to 'MovieMania'. Great!πͺπ
So, one more thing I learned is that, for example, if we console.log
the title in the cleanup
function when the component unmounts.π
So, since the cleanup happens after the component unmounts, we should probably expect to get undefined
for the title in the console, right? However, even though the title changes back to 'MovieMania' after the component unmounts, we still see the title as 'The Beekeeper' in the console.ππ
So, it might seem strange that the function remembers the movie title, given that it runs after the component has unmounted.
This behavior is explained by a fundamental concept in JavaScript called a closure. Essentially, a closure means that a function will always remember all the variables that were present at the time and place the function was created. In the case of our cleanup function, it was created at the time the effect was first created, when the title was defined as 'Beekeeper'. Therefore, this function closed over the title variable and will remember it even after the component has unmounted.
it's worth noting that the cleanup function runs between renders, meaning it executes after each render. So, when we click on different movies, the cleanup function runs for each movie after its corresponding render.
Cleaning up data fetching
We're encountering a bit of a snag with our data fetching process. Right now, it feels like we're bombarding the server with way too many requests whenever we search for movies.
Let me walk you through it. If we make this window bigger and take a peek at the network tabπππ, you'll see what I mean. With each keystroke, it's like we're firing off a whole new set of requests all at once. It's overwhelming. And the trouble is, this creates a couple of issues for us:
firstly, each request slows down because there are just so many of them, and
secondly, we end up pulling down way more data than we actually need.
Think about it β imagine if one of those requests takes longer than the others to come back. We'd end up getting results from that slower request, even if it's not the most recent thing we typed. That's what they call a race condition, where requests are all competing to be the fastest. It's a real headache!π€
To tackle this issue, we'll use a native browser API called an AbortController and integrate it into our cleanup function.
First off, we initiate this process by creating an AbortController. We achieve this by defining a new variable, let's call it controller
.ππ
Then, we connect it to our fetch request by adding it as a signal in the fetch options.ππ
In the cleanup function, which executes after each keystroke, we'll use controller.abort()
to terminate the current request. This guarantees that only the most recent request is processed, while any previous ones are disregarded or ignored.π
Resultπ
Let's delve into why this π€ππ approach is effective.
Each time a keystroke is detected, the component undergoes a re-render. During these re-renders, the cleanup function is called. This timing is crucial because it allows us to abort the current fetch request whenever a new keystroke triggers a re-render. Thus, the cleanup function serves as the ideal place to handle this task between renders.π
However, there's a caveatππ. When a request is canceled, JavaScript interprets it as an error. Consequently, our application experiences errors triggered by these canceled requests. Specifically, the error message reads: "The user aborted a request". Essentially, when a fetch request is canceled, it throws an error, which is then caught in our catch block. As a result, we also see these errors being logged.
To address this, we implement a conditional check: if the error's name is not "AbortError," we set the error accordingly. This ensures that only relevant errors are handled, as this specific issue isn't a problem.π
Also, remember to reset the error to an empty string after setting the movies. This ensures that any previous errors are cleared before new requests are made. So, we set the error to an empty string both at the beginning and at the end. It seems like I already have this handled. π
And with that, no more race conditions and no unnecessary data being fetched. If at some point in the future, you're going to make your own HTTP requests in an effect like this, always remember to clean up after your fetch requests, especially in situations where many requests can be fired off rapidly, one after another, just like we've encountered here. Typically, when we click on one of the movies and the data gets fetched, we won't have a flurry of requests occurring one after another.π
Listening to keypress
Let's now implement a new small feature that requires us to globally listen to a keypress event. It's quite simple: when we open up a movie to see the details, instead of clicking on a button to go back, we want to allow the user to simply press the Escape key.
To achieve this, we need to listen for the keypress event globally.
So, let's dive in. The way we can react to a keypress event across the entire app is by attaching an event listener to the entire document. We'll do this directly in the App
component. Since this involves directly interacting with the DOM, it's clearly a side effect, which means we need another effect hook.
By adding this effect, we're stepping outside of React's usual paradigm and directly manipulating the DOM, which is why the React team refers to the useEffect
hook as an "escape hatch" β a way to write code that doesn't strictly adhere to React's conventions.
Alright, let's listen for the keydown event. We'll pass a callback function that receives the event object. Then, we'll check if the event.code
matches 'Escape'. If it does, we'll call our handleCloseMovie
function.ππ
Here I add a console.log
statement to show "close" in the console when the Escape key is pressed .π
So again, as I keep hitting the Escape key, we see by this log that actually this callback function, the event listener, is still listening for the keydown event and it will then execute this function each time a keypress happens, which is, however, not really what we want in this situation because we actually don't even have a movie opened here.π
So basically, what we want instead is to only attach this event listener to the document when we actually have the movie details in our tree, meaning when that component instance is actually mounted. That's easy enough. We just cut the effect from here. After all, we want it in our MovieDetails
component. Alright, I've started to place it there so that we could understand why we actually need it here.
And now here, of course, this function is called onCloseMovie
, which, again, we had already passed into this component. Now, you see that ESLint is complaining, and the reason for that is that we must include this function in our dependency array as well. So, that doesn't seem to make a lot of sense, but we will later learn why that is.
So again, when React tells us that we need to include something in the array, we must actually do that. Otherwise, there might be consequences that we do not want.ππ
And so, whenever you see a warning from ESLint about a missing dependency, you must include that in the array. Otherwise, React will complain that you are lying about your dependencies, which, of course, we don't want.ππ
But anyway, let's not try to hit the Escape key again while the MovieDetails
component is not mounted. So now, we don't get that console.log, and therefore, this function is, of course, not being executed.
And so, if we open up 10 movies and then close them all, we will end up with 10 of the same event listeners attached to the document, which, of course, is not what we want.
This means that here, we also need to clean up our event listeners, or in other words, we need to return a function here that will execute document.removeEventListener
. Basically, as soon as the MovieDetails
component unmounts, the event listener will be removed from the document, avoiding the accumulation of event listeners in our DOM, which could become a memory problem in a larger application with hundreds or thousands of event listeners.
So, in this small app, of course, this wouldn't be a problem, but it's important to prepare for real-life scenarios.
Now, the function that we pass in to remove the event listener must be exactly the same as the one we used in addEventListener
. We cannot simply copy and paste this function; it must be the same one.π
So, this is how we handle keypress events in a React application. Again, we need to step out of the React way and back into classical DOM stuff. For that, we use an effect. We specify our effect, listen for the event, and each time the component unmounts or even each time it re-renders, we will then remove the old event listener from the document
Thank you for readingπ