A closer look at UseEffect — Part 2

Paola Dolcemascolo
6 min readFeb 18, 2021
Let’s talk cleanup!

In the first part of this two part series (link here), I started a deep dive into the useEffect hook. We built a search bar that called on the Wikipedia API to return a list of results based on a user’s input…and then we displayed those results. The end product looked like this:

Results for simple search bar calling on the Wikipedia API

At this point, the results don’t really mean that much. We can only see a small snippet of each entry…and presumably, we did a search to actually get some detailed information, not just an incomplete snippet. What COULD be helpful is if those snippets acted as “previews”, let’s say for a user to check out; a user could then CLICK on whichever preview seemed more appropriate and be taken to the actual Wikipedia page for that entry. How about, I don’t know, a lovely BUTTON?

This isn’t going to be too difficult, given the code we already wrote in Part 1 of this series. We’ll just have to add some code to style the button and give it an anchor tag to direct the user to the appropriate page.

So our renderedResults constant (which maps over all of our results and properly styles them into our list) becomes this:

The code in red is what we just added for our button

And our page looks like:

Notice that when we hover over a button (in this case the first one), at the bottom left hand corner of the browser we see a preview of what Wikipedia page we will be taken to. And if we click on that button, we are directed to the appropriate article.

Great! At this point, though, we have our component set up in such a way that for each character that the user inputs, an API call is being made. This is EXTREMELY INEFFICIENT.

What we would actually LIKE to happen is that a user types in as much as they want, and then some amount of time MUST pass during which the user makes no additional input changes…THEN the search is carried out.

So if our useEffect call looks like this:

We could wrap our if statement in a setTimeOut():

const timeoutId = setTimeOut(() => {
if(term) {
search();
}
}, 1000);
}, [term]);

Notice that I assigned the setTimeOut() function to a constant, timeoutId. This is needed so that we can reference our timer and pass it into a clearTimeOut() function, in order to cancel that timer. The next big question is how to get that timeoutId and cancel it the NEXT time a user types something in. Do we create a new piece of state to keep track of that timeoutId? We definitely could do that…but there is a slightly better approach that makes use of another feature of the useEffect hook.

The Cleanup Function

Whenever we call useEffect and we provide a function as the first argument, there is only one possible value that we are allowed to return from this arrow function…and that is another function.

useEffect(() => {
console.log("Hello!");

return () => {
console.log("CLEANUP");
};
}, [term]);

The goal of that arrow function is essentially to do some kind of ‘cleanup’. In order to understand how this function works we need to understand when it is that React will call this function automatically (yes, automatically!). You see, when we return this ‘cleanup’ function, React keeps a reference to it in order to call it at some future point in time (again, automatically!).

So how does this work? When our component first renders, the overall arrow function passed into useEffect is invoked and we return the ‘cleanup’ function. When it is time to run the overall arrow function again, React FIRST invokes the cleanup function that it got from the last time useEffect ran and then it calls the overall arrow function again. And this happens over and over again.

If we run the above code, we would just see a “Hello!” on initial render…then whenever we change our term, we would see “CLEANUP”, followed by another “Hello!”

This ‘cleanup’ function is going to be the key to how we cancel our previous timer in our example. There is only one problem, though. We need to make sure that there is NO delay when we first render our component.

In order to make sure we have results displayed on initial render we’ll need to add some conditional logic:

Notice that little squiggly yellow line under term…

Our component is now working; results show up immediately upon initial render and API calls have been throttled so we are avoiding making calls every time the user presses a key.

BUT.

Whaaaaaa…??

What’s that new warning???

Any time that you refer to a prop or piece of state inside of useEffect, React is going to want you to reference any different prop or piece of state inside of the useEffect dependency array (the array that controls when useEffect gets executed). In fact, if we add results.length after term in our dependency array:

No yellow squiggly line!

No warnings pop up, either in our code or in our console.

But. This is NOT the ideal solution, because it introduces some weird bugs (and we also don’t want to keep arbitrarily adding items into our dependency array)…in particular, on our initial render, we will make a call to our API twice because the value of results.length will have changed.

In order to tidy this all up, we’re going to have to make a fairly significant change to the structure of our Search component. We are going to add another piece of state and the second piece of state is going to keep track of our time-lagged search term, which we will call debouncedTerm.

At our initial component render, we are going to set term equal to whatever we want our default search value to be (in our case, ‘programming’) and we will introduce our second piece of state, debouncedTerm, and ALSO set it initially to the same default value, ‘programming’.

Here’s the interesting part…we are then going to set up TWO DIFFERENT useEffect FUNCTIONS! One is going to have debouncedTerm listed in its dependency array, while the other will have term listed in its dependency array…therefore, each will have a separate piece of state it is watching.

Our request logic (our call to the API) will be placed in the useEffect for debouncedTerm. We will then run some code to immediately update our term piece of state whenever the user types something in. At this point, then, we’ll have a timer set up…but instead of having it set up to make a request, like we did above, our timer will be set up to update the debouncedTerm piece of state. So this debouncedTerm will ONLY be updated after a user stops typing for whatever amount of time we designate our timer to run for. Once that amount of time has elapsed, debouncedTerm will be updated, which will cause a re-render and the useEffect responsible for watching debouncedTerm runs…and THIS then runs our data fetching function.

Here, then, is our completed Search component.

And that ends my two part series on useEffect! The Search component wasn’t anything super fancy, but it definitely helped me learn a lot more about how useful this hook can be.

--

--