The useEffect hook lets you perform side effects in function components
What are side effects?
Side effects are not specific to React. A side effect is anything that affects something outside the scope of the function/component being executed. Anything that is not the return value is technically a side effect.
A few common examples of side effects
- Data fetching/Network requests
- Setting up a subscription to an external data source
- Manually changing the DOM
- Accessing the Window object
Basic Syntax
The useEffect
hook accepts two arguments: the side effect callback function, and an optional dependency array of state values to watch for changes.
useEffect(sideEffectFunction, [stateToTrack]);
By using this hook, you tell React that your component needs to do something after render. React will remember the effect function you provided, and run it after flushing changes to the DOM and letting the browser paint the screen.
By default, useEffect
runs after the first render and after every update. Effects happen after render. React guarantees that the DOM has been updated before it runs the effects.
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0)
const min = 0
// Register the effect you want to run
useEffect(() => {
// Update the document title using the browser API
document.title = `The current count is ${count}`
})
const increment = () => setCount(count + 1)
const decrement = () => (count > min ? setCount(count - 1) : count)
const reset = () => setCount(0)
return (
<div className='counter'>
<p className='count'>{count}</p>
<div className='controls'>
<button type='button' onClick={increment}>
Increment
</button>
<button type='button' onClick={decrement}>
Decrement
</button>
<button type='button' onClick={reset}>
Reset
</button>
</div>
</div>
)
}
export default Counter
In the above counter example, we declare the count
state variable and set its initial value to 0. We then tell React we need to use an effect for updating the document title.
We pass a function to useEffect
, which is our effect that we want to be run after the component renders. Inside of our effect, we set the document title using the document.title
browser API. Remember, accessing the browser API and manipulating the DOM directly is a side effect.
The above effect is run on every render, including the first one.
Skipping Effects
The effect hook runs when the component mounts but also when the component updates. In the counter example above, the effect is run on every single render which is ok in this case because count
is our only piece of state and we want our effect to be run whenever count changes. This is almost never what you want.
Let's look at an example where not passing an array of dependencies and allowing the effect to be run on every render would cause us some serious trouble.
const Repos = () => {
const [userName, setUserName] = useState('')
const [repos, setRepos] = useState([])
useEffect(() => {
async function fetchRepos() {
const response = await fetch(`https://api.github.com/users/${userName}/repos`)
const repos = await response.json()
// our setRepos call tells React to re-render the component.
// which then calls our useEffect hook again, so on and so forth
setRepos()
}
fetchRepos().catch(error => console.error(error))
// this is because we are not passing an array of
// dependencies as the second argument to useEffect
})
const handleSubmit = (e) => {
e.preventDefault()
setUserName(e.target.username.value)
};
return (
<>
<form onSubmit={handleSubmit}>
<label htmlFor='username' placeholder='E.g. gaearon'>
Enter a Github Username
<input type='text' id='username' />
</label>
<button type="submit">Fetch Repos</button>
</form>
<section aria-labelledby='repos-label'>
<h2 id='repos-label'>Github Repositories for {userName}</h2>
{!repos.length ? (
<p>
<b>Not seeing any repos? Either there are no repos for the user you have provided, they do not exist, or there was an error while fetching. Please try again with a different username.</b>
</p>
) : (
<ul>
{repos.map(repo => (
<li key={repo.id}>
<a href={repo.html_url}>{repo.name}</a>
</li>
))}
</ul>
)}
</section>
</>
);
}
The above example is making a network request for an array of Github repositories for a given username, then spits out a list of links pointing to those repos. When the effect is run, it sets our repos state variable, which tells React to re-render our component, which then triggers our effect which tells React to re-render, so on and so forth sending us into a death loop of renders and network requests until either our browser stops responding or we hit our rate limit of 5000 requests to the GitHub API per hour.
So, we don't want to let our effect run after every single render. One option to prevent this death loop is to pass an empty array of dependencies as the second argument to useEffect
. This would tell React to only run our effect on the very first render.
...
useEffect(() => {
async function fetchRepos() {
const response = await fetch(`https://api.github.com/users/${userName}/repos`)
const repos = await response.json()
setRepos()
}
fetchRepos().catch(error => console.error(error))
// Passing an empty array of dependencies tells React
// to only run our effect on the very first render
}, [])
...
As you probably guessed, this is also NOT what we want as we would like to fetch a new list of repos when we submit our form. With an empty array, submitting the form which updates our userName
in state, would not make a new request for the updated user's list of repositories as our effect is only run once, on the very first render.
So, we don't want our effect to be run when repos
value is updated and we also don't want it to only run on the very first render. Our solution is to add userName
as the only dependency to our effect.
...
useEffect(() => {
async function fetchRepos() {
const response = await fetch(`https://api.github.com/users/${userName}/repos`)
const repos = await response.json()
setRepos()
}
fetchRepos().catch(error => console.error(error))
// Now our effect will only run if the value of userName in state is updated
}, [userName])
...
Here is the full solution to our Repos component.
const Repos = () => {
const [userName, setUserName] = useState('')
const [repos, setRepos] = useState([])
useEffect(() => {
async function fetchRepos() {
const response = await fetch(`https://api.github.com/users/${userName}/repos`)
const repos = await response.json()
setRepos()
}
fetchRepos().catch(error => console.error(error))
}, [userName])
const handleSubmit = (e) => {
e.preventDefault()
setUserName(e.target.username.value)
};
return (
<>
<form onSubmit={handleSubmit}>
<label htmlFor='username' placeholder='E.g. gaearon'>
Enter a Github Username
<input type='text' id='username' />
</label>
<button type="submit">Fetch Repos</button>
</form>
<section aria-labelledby='repos-label'>
<h2 id='repos-label'>Github Repositories for {userName}</h2>
{!repos.length ? (
<p>
<b>Not seeing any repos? Either there are no repos for the user you have provided, they do not exist, or there was an error while fetching. Please try again with a different username.</b>
</p>
) : (
<ul>
{repos.map(repo => (
<li key={repo.id}>
<a href={repo.html_url}>{repo.name}</a>
</li>
))}
</ul>
)}
</section>
</>
);
}
useEffect & Cleanup
Sometimes, we want to run some additional code after React has updated the DOM. Network requests, DOM mutations, and logging are common examples of effects that donβt require cleanup. We say that because we can run them and immediately forget about them.
Anything that we set up that is recurring such as an interval, subscription, websocket connection, etc. needs to be cleaned up when the component unmounts.
Let's add a twist to our counter component...
const Counter = () => {
const [count, setCount] = useState(0)
// Log the count to the console after 3 seconds
// This effect is not cleaning up after itself
useEffect(() => {
setInterval(() => {
console.log(`Count: ${count}`)
}, 3000)
// not returning a cleanup function here
}, [count])
...
The problem with not cleaning up our setInterval()
is that each time the component re-renders, we register another interval. If we were to update the count
from 0 to 1, after three seconds, 0 would be logged to the console then 1 would be logged to the console, then 0, then 1 and so on..
This is because there are now two intervals from two separate renders logging the value of count to the console. Each interval has access to the value of count
from its respective render. When the component first rendered, the value was 0, so an interval was started to log 0 every three seconds. When we updated count
to 1, React triggered another render, then our effect was called, registering another interval to log the new value of count
to the console every 3 seconds.
The first interval was never cleared, so now we have two intervals running at the same time.
To avoid this, we need to return a cleanup function from useEffect
for our interval.
const Counter = () => {
const [time, setTime] = useState(new Date())
// Log the count to the console after 3 seconds
useEffect(() => {
const id = setInterval(() => {
console.log(`Count: ${count}`)
}, 3000)
// Return a function to clear our interval when the component unmounts
return () => clearInterval(id)
}, [count])
...
setInterval
returns a number which is the id of that interval. We set that number to a variable which we pass to the clearInterval
function returned from our effect. Now, when count
is updated and our component is unmounted before re-mounting to the DOM, we clean up the previous interval. With this in place, only the current value of count
will be logged to the console 3 seconds after updating its value.
Recap
- The
useEffect
hook lets you perform side effects in function components; - A side effect is anything that affects something outside the scope of the function/component being executed;
- The
useEffect
hook accepts two arguments: the side effect callback function, and an optional dependency array of state values to watch for changes; - By not passing a dependency array to our effect, it will be run on every single render;
- If we pass an empty array, the effect will only be run once, on the very first render;
- To avoid an infinite loop of renders and effect calls make sure you are only passing the state values that your effect depends on in the dependency array;
- Intervals, subscriptions or anything that is meant to be recurring, should be cleaned up by returning a cleanup function from your effect;
Thanks for reading!
Top comments (1)
I found this really helpful, thanks.