This site runs best with JavaScript enabled.

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 action
return () => {
// 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 action
return () => {
// 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 action
return () => {
// 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 action
return () => {
// 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 way
const [value, setValue] = useState("")
useEffect(() => {
setValue(value)
}, [])
// right way
useEffect(() => {
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])
// or
useEffect(() => {
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 this
useEffect(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!

Share article

Join the Newsletter

This just means you'll receive each weekly blog post as an email from me and maybe an occasional announcement 😀


Disclaimer: Views and opinions expressed on this blog are my own and are in no way representative of my employer.