React Hooks Revisited: useEffect
ryanharris.dev
Posted on September 8, 2020
In my last article, we learned about one of the most commonly used hooks, useState
. This time around, we are going to look at another commonly used hook: useEffect
:
By using this Hook, you tell React that your component needs to do something after render. React will remember the function you passed (we'll refer to it as our "effect"), and call it later after performing DOM updates. ~ React docs
What are effects?
Effects, a shorthand for "side effects", represent component operations or actions that cannot be done during the render phase. Examples of this can include:
- Fetching data from an API
- Setting up data subscriptions or document event listeners
- Manipulating the DOM
We can also further classify them into two categories:
- Effects that require cleanup
- Effects that don't
For example, if we attach an event listener to the document, we will want to remove it when the component unmounts as this will help with performance and avoid conflicting listeners. On the other hand, something like updating the document.title
does not require any further work when the component unmounts.
To make cleaning up effects easy, the useEffect
API allows you to optionally return a function from the hook, which does the work of removing listeners, subscriptions, etc. Previously, you would've needed to leverage both the componentDidMount
and componentDidUnmount
lifecycle methods to achieve this whereas useEffect
allows us to take care of it all at once.
Anatomy of useEffect
Now that we've talked about what useEffect
does, let's take a look at the syntax:
useEffect(() => {
// 2. This function body is your effect
window.addEventListener("resize", handleResize)
return () => {
// 1. Optionally clean up effects inside this function
window.removeEventListener("resize", handleResize)
}
}, []) // 3. Conditionally execute based on dependencies array
If this syntax looks a bit strange, don't worry. We'll break down each piece before moving on to some practical examples. Let's start with the optional cleanup function since we were just talking about it.
1. Cleanup
Inside our effect, we can optionally return a function. This function will perform any cleanup work we want to happen when this component unmounts. In our example, we are removing the event listener from the window to make sure it doesn't keep listening/firing after the component is no longer in the DOM.
2. The effect
The first argument useEffect
takes is a function. This function is your effect and defines the work you want to do whenever the component mounts. In this case, we are simply adding an event listener to the window that executes the handleResize
function on resize
.
3. Dependency array
The optional second argument in this example is what's known as the "dependency array". Essentially, leveraging this array allows you to control the conditional execution of the effect based upon changing prop or state values in the component. We will talk about this more in-depth in the next section.
What is the dependency array?
By default, effects run after every completed render, but you can choose to fire them only when certain values have changed. ~ React docs
As I mentioned above, the dependency array is an optional secondary argument passed to the useEffect
hook. Its purpose is to allow you to more easily control the execution of your effects based upon the values within you component. In class components, we would most likely need to use the componentDidUpdate
lifecycle method to achieve the same results, which would've looked something like this:
componentDidUpdate(prevProps, prevState) {
if (prevState.cardTypes !== this.state.cardTypes) {
// Your effect logic would live here
}
}
Using the dependencies array we can do things like:
- Fire the effect everytime the component renders
useEffect(() => {
const cardTypes = fetchCardTypes()
setCardTypes(cardTypes)
})
- Fire the effect only upon the first render
useEffect(() => {
const cardTypes = fetchCardTypes()
setCardTypes(cardTypes)
}, [])
- Fire the effect only when certain prop or state values have changed
useEffect(() => {
const cardTypes = fetchCardTypes()
setCardTypes(cardTypes)
}, [cards])
One thing to note here is that while you can also use if
statements within your useEffect
hooks to conditionally execute logic, you cannot wrap hooks in if
statements. Part of how React keeps effects predictable is to run them all upon render. Skipping effects like this is considered bad practice, so don't do it!
In practice
In the sandbox below, I've create a small application that leverages useEffect
in numerous ways in order to serve us information about the latest weather on Mars. Elon Musk, eat your heart out!
For simplicity's sake, I've created two components: App
and WeatherDisplay
. The former handles fetching our data from the Nasa API and our application logic, whearas the latter simply displays the data we've passed down to it as props. Because of that, all of our useEffect
hooks live inside of App
.
As you will notice, we actually have three useEffect
hooks inside of our component, which may seem a bit strange, but is the whole idea of hooks! This allows us to compartmentalize our component logic and more easily reason about the effects they trigger. In this example, our three hooks are doing the following:
- Hook #1 sets the title of our document on every render using the value of our
day
state - Hook #2 fetchs our API data only on the first render as we don't want to make be making continually API calls as the component updates
- Hook #3 parses the proper data object based on the value of
day
any time the value ofday
ordata
change
Each of these hooks is using a diffrent variation of the optional dependency array we discussed earlier. Look at the code a bit more closely -- do you know why each array looks the way it does?
Don't worry if you're still a bit confused because learning to "think in hooks" can take some time. The best way to get more comfortable with them is to use them, so feel free to fork the sandbox above and play around with the code. As a good place to start, try removing the dependency array from Hook #2 completely. What happens?
With an empty dependency array, the effect makes a request for our API data on mount and this only happens once. Previously, we would componentDidUpdate
and compare the components prevProps
to it's current props
and use that to determine if there was work to be done. Now, with useEffect
, we no longer have to do this and can just define what prop values we care about and run the effect only when one of them changes. We'll talk more about this more later in the series.
If we remove the dependency array altogether, the effect runs on every render, which means we're making API calls every time the component re-renders. Since something simple like changing state (ex. clicking on the Today
or Yesterday
buttons) causes a re-render, we'd essentially fetch data everytime the user clicked one of the buttons. This is not good for application performance, nor your API bill.
Ultimately, hooks are meant to compartmentalize application logic to make them easier to re-use and reason about. useEffect
is no different. Like all hooks, it runs on every render by default, but unlike other hooks, provides a way to conditionally control our logic execution (i.e. the dependency array). While useEffect
is often described as componentDidMount
, componentDidUpdate
and componentWillUnmount
altogether, I would try to avoid thinking about hooks in terms of their equivalent lifecycle methods. Instead, identify the effects that need to take place in your component, determine how often your would like the effect to run and cleanup your work (if needed) when the component unmounts.
Try adding useEffect
to your next component. Maybe you'll get hooked.
Posted on September 8, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 2, 2024