React's useEffect Hook
Last Updated October 27, 2020
useEffect
is the replacement for all of the lifecycle methods in class components - think componentDidMount
, componentDidUnmount
, etc. However, it's not just a replacement for lifecycle methods. useEffect
can be used for more than that. We have to shift our thinking from useEffect
is just a replacement for lifecycle methods to useEffect
allows us to synchronize our state. Let's take a look at how it's written.
useEffect(() => {// some actionreturn () => {// cleanup}}, [])
useEffect
takes a function that runs every time the effect is triggered. The return
statement is optional. The return
is essentially only for cleanup. For example, let's say in your effect you create a timer with setTimeout()
. You'd want to clean that up otherwise you'll have a memory leak.
useEffect(() => {const destroyTimer = setTimeout(() => action(), 1000)return () => {destroyTimer()}}, [])
Another common scenario would be using something like the RxJS library and subscribing to an Observable
. Observables always to need to unsubscribed, lest you'll create a memory leak. That's another use-case for the return
statement in useEffect
.
You'll notice that useEffect
not only takes a function as a parameter, it also takes an array. That array is the list of items, sometimes referred to as dependencies, to watch so that the effect knows when to run. You can think of the items in the array as triggers. Each time an item changes, the effect is triggered. In our previous examples that array has been empty. There are essentially three states for that array to be in, and each state tells the effect to behave differently. Let's look at those.
Empty array
useEffect(() => {// some actionreturn () => {// cleanup}},[] /* runs once on render */)
An empty array gives nothing for the effect to trigger from. That means the effect will run once and not run again until the component is re-rendered. You can think of the []
as the replacement for componentDidMount
. Notice I didn't say componentWillMount
. Effects are ran after the component has been mounted.
No array
useEffect(() => {// some actionreturn () => {// cleanup}})
Interestingly enough, the array is optional. The effect of this scenario is even more interesting. When there is no array, useEffect
just runs on every re-render.
You will likely rarely want to do this.
Array with items (dependencies)
useEffect(() => {// some actionreturn () => {// cleanup}}, [value1, value2])
When useEffect
has items in the array it treats those items as triggers. Sometimes you'll hear these items referred to as dependencies. Each item is watched, waiting for it to change. When a change occurs in any one item the effect then runs.
Common Use Cases
useEffect
is very flexible and can certainly be used for all kinds of things. However, it's most common use case is data fetching. Data fetching is perfect for useEffect
because it only fires after the component has mounted and it gives us the ability to control when the data fetching is re-triggered, if at all.
useEffect(() => {let apiData = []const fetchData = async () => {try {const response = await fetch(`<url>`)apiData = response.json()} catch (err) {console.log(err)}}fetchData()}, [])
This is a contrived example that probably isn't very realistic. Most likely, you'd be modifying some state, as opposed to a value defined inside the effect like apiData
. However, it serves the purpose of showing how an API call might look using useEffect
.
Best Practices
Referenced Values Should Be Dependencies
This means that for every value you reference in the effect function it should be listed as a dependency.
// wrong wayconst [value, setValue] = useState("")useEffect(() => {setValue(value)}, [])// right wayuseEffect(() => {setValue(value)}, [value])
Note we didn't reference setValue
as a dependency. That's because the method will always be static because it came from useState
. Because it's static we didn't reference it, however it would be totally fine to add it to the dependency array [value, setValue]
. However, if another method were that we created, say, updateValue
, then we would want to include it in the array.
const updateValue = (value) => setValue(value)useEffect(() => {updateValue(value)}, [value, updateValue])
Actions & Updates Should Be Decoupled
You may be thinking, "Wait a minute, haven't we been putting update logic in our effect action method this whole time?!" And you'd be correct. We have been. Actions and updates should be decoupled is not a hard and fast rule. It's more a pattern to be aware of. When you only have one value as a dependency not much can go wrong, so it's kosher to just update it directly from the effect itself. However, when dependencies start to grow, a need arises to decouple our update logic from action logic.
What does that really mean, though? It effectively means reducing the number of dependencies by abstraction. This is typically achieved by using the useReducer
hook. Since we haven't covered that hook yet - trust me, it warrants its own explanation outside of this scope - we won't dive any deeper into this best practice. It'll be covered in more detail in the useReducer
lesson.
Functions Only Used By An Effect Should Live In The Effect
I'll admit, this one looks a little weird. But, it makes sense. If a function is referenced in useEffect
and it is only referenced there, then the function should live inside the effect itself.
useEffect(() => {const updateValue = (value) => setValue(value)updateValue(value)}, [value])
This is good for two reasons:
- it keeps us from breaking the Referenced Values Should Be Dependencies best practice.
- it clearly shows the method is not used anywhere but inside the effect.
Gotchas
There are some gotchas with useEffect
that need to be discussed. One is indirectly related, the other is directly related.
The setState gotcha
The useState
hook runs asynchronously and therefore ought to be used with caution inside useEffect
. What do I mean by this? Let's look at the following example:
useEffect(() => {setValue('new')if (value === 'new') {// do something}}, [value])
Will that conditional return true
or false
? Well, since setState
is asynchronous we can't be certain. So, you should avoid this at all costs.
The alternative is to either use a locally scoped const
or let
defined variable or create another useEffect
that listens for changes to value
.
useEffect(() => {const tmp = 'new'setValue(tmp)if (tmp === 'new') {// do something}}, [value])// oruseEffect(() => {setValue('new')}, [])useEffect(() => {if (value === 'new') {// do something}}, [value])
The async gotcha
Earlier when I mentioned data fetching I used the following code example:
useEffect(() => {let apiData = []const fetchData = async () => {try {const response = await fetch(`<url>`)apiData = response.json()} catch (err) {console.log(err)}}fetchData()}, [])
Notice I defined my function inside the useEffect
hook. I did not do this:
const fetchData = async () => {try {const response = await fetch(`<url>`)apiData = response.json()} catch (err) {console.log(err)}}// you can't do thisuseEffect(async () => {let apiData = []await fetchData()}, [])
Now, I've tried. Why? Because that's what makes sense. If I want to call an async
method just use await
inside the effect and make the effect method async
itself, right? Right?! No.
This won't work, and here's why. Remember earlier we talked about the return
statement on useEffect
? That's extremely important here. When the React team created useEffect
they needed it
to return a cleanup method, naturally. Well, that flies in the face of what async
methods return. They return promises! Always! So, the two are by default in conflict with one another. That'say
why you cannot make useEffect
itself asynchronous. You must use a workaround. Well, that's not really fair. It's not a workaround. It's just a way of solving your problem that maybe isn't immediately
intuitive. And that's okay.
Summary
We've covered a lot. I hope this article will serve as a reference for when you run into useEffect
troubles. There's still a lot more to unpack, though. And I'll be covering some of those things in the weeks
to come. Until next time!