If you've learned the traditional class-based React Components and you're now trying to move into Hooks there's a few things along the way that'll throw you for a loop.
One such thing that took a little digging for me is the combination of useEffect()
- essentially the replacement for componentDidMount
, componentDidUpdate
, and componentWillUnmount
- and async
/await
.
The Situation
Common scenario: Our app loads, we get something on the screen AND THEN we want to fetch some sort of data. We don't want to block the User from moving on or make them stare at a blank screen because our API call is taking longer than expected.
componentDidMount
This is the method in which we remedied this problem if using React Class-based Components. It ensures that the actual Component is inserted into the DOM tree first and then render()
is called.
If we don't need an API request to be made then the Component will simply render to the screen. If we do need to make an API request we should do that inside of componentDidMount()
. In addition if upon receiving the API response we must update, or setState()
, then that will actually trigger a second render.
Then the calls will look like this:
- componentDidMount()
- render()
- setState() [from inside componentDidMount()]
- render()
Even though there is a second render, the React Docs state that the user will not see the intermediate state. So no weird flickering UI - that's good for us! However, the Docs also say to be cautious as this way could lead to performance issues.
More specifically, any DOM node that needs insertion into the tree belongs in this spot, componentDidMount()
. If you can React recommends that initialization of state
be done in the constructor()
instead of here. Obviously, that isn't always viable but it is what they recommend.
// src/api/index.js
export default {
async index() {
const res = await fetch('https://my-json-server.typicode.com/Claim-Academy-JS/products/products')
return await res.json()
}
}
/* ------------------ */
import api from 'api'
async componentDidMount() {
const products = await api.index()
this.setState({ filteredProducts: products, products })
}
This code shows the fetch call being made inside componentDidMount()
and in this case for my project I needed this setup. These products were to be inserted into the DOM tree so I make the fetch call and set my state. Of course async
is on both componentDidMount()
and my exported object's index()
method. Then inside both of those we await
the results.
useEffect()
Now with Hooks, more specifically useEffect()
there's a couple things we need to be aware of. First we must understand that useEffect()
takes two arguments.
First Argument
- Callback function
Second Argument
- Property to watch for a change -> then fire the provided callback
So as per usual, a callback is needed - no surprise there. The second parameter can cause some problems if we forget to specify it. If there is no second argument provided the useEffect()
will fire on every update no matter what is being updated. Even further, if there's a setState()
or a useState()
setter being used inside - useEffect()
will go into an infinite loop.
Let's take a look at an equivalent code to the previous example just instead utilizing useEffect()
:
useEffect(() => {
(async () => {
const products = await api.index()
setFilteredProducts(products)
setProducts(products)
})()
}, [])
This time you see there is an Immediately Invoked Function Expression, or IIFE, inside. We could just as well name that function and then specifically invoke it inside too.
useEffect(() => {
const fetchProducts = async () => {
const products = await api.index()
setFilteredProducts(products)
setProducts(products)
}
fetchProducts()
}, [])
Also take note, we are actually providing a callback to useEffect()
and within that callback we must define another function and invoke it. That's due to a fetch call returning a Promise. So essentially useEffect()
itself isn't responsible for that so our defined function will handle it.
Lastly that second argument is present to ensure this useEffect()
only runs at a specific time.
There we provided []
as the second argument. This is telling useEffect()
"Hey, I only want you to run your callback when this Component mounts for the first time and that's it." By using []
we tell useEffect()
that there are no properties we want you to watch and then run your callback when they change. Just run once.
There's a difference too between []
as a second argument and no second argument. As mentioned before without a second argument that useEffect()
will think it should run when the component mounts and then on every single update after regardless of what piece of state changes.
If we wanted a function to run each time a specific piece of state changes you would simply put it inside the brackets like so [somePropertyNameHere]
.
Article Will Unmount
It took a little digging to find the way to combine useEffect()
and async
functions. It was definitely surprising the the React Docs has no examples of this situation at all that I could find. I referenced a few different 3rd party articles explaining this to be sure I was on the right path by what all of them said. Robin Wieruch wrote about this; it was the first one and one of the better articles I found on this topic.
Definitely happy to figure it out because even though this was a weird one to figure out - I still like Hooks more!
Top comments (8)
Good write up on this. Have you seen/used the useAsyncEffect hook? It has a very similar signature as
useEffect
so it's a simple drop in.I hadnβt seen that before. It doesnβt seem too much simpler at first glance but I will say the differences when needing to
destroy
isnβt something Iβve worked with. So it could be better in those cases.You can use also the use callback hook Inside the use async or use promise chaining inside the use effect
Iβm still cutting my teeth with the basic hooks. But that is good to think about because Iβm sure Iβll start running into use cases for the other hooks such as the ones you mentioned.
Thanks very much. This is a really helpful article.
Wow Clear, simple and straight to the point. πͺπͺ Would read xUnlimited amount of time when I am using useEffect and forgetting the effect.
wow what an explaination!!
Thank you for a simple and clear explanation πππ π