What is useEffect?
useEffect
is a react hook that lets you run side effects inside a functional component. Side effects can be any operation that does not interfere with the main execution of the component, like:
- Directly manipulating the DOM.
- Fetching data from an API in the background.
- Running a function after a certain amount of time using
setTimeout
or at each interval usingsetInterval
.
The syntax
useEffect
has the following syntax:
useEffect(
() => {
// the callback function which has the side effect you want to run
return () => {
/* this is an optional cleanup callback,
which will be called before the next render */
}
},
[
/* this an optional array of dependencies.
The useEffect callback runs only when these dependencies change*/
]
)
It might look overwhelming at first sight. Do not worry!
In this tutorial, we will break it into pieces and learn all the practical combinations and applications of useEffect
.
The simplest useEffect
Since the only mandatory parameter of an useEffect
is the callback function, let's write one with just the callback:
import { useEffect, useState } from "react"
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log("Running useEffect")
document.title = `You clicked ${count} times`
})
console.log("Running render")
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>Click Me</button>
</div>
)
}
export default App
In the above example, we are having a button, when clicked will increment the count
by 1. Then we have written a useEffect
hook where we console log "Running useEffect" and update the title of the page (direct DOM manipulation) with the number of clicks.
If you run the code and open the browser console, you should be able to see the logs as shown below:
As you could see, first the component will be rendered and then the effect will run. Now, if you click on the button, you will see that the component is rendered again (since the state has changed) and the title of the page is updated with the number of clicks.
From this, we can infer that the useEffect
(with only a callback function) will run after each render.
Infinite Loops
Since useEffect
runs after every render, what if the effect inside useEffect
causes the component to re-render? That is, if the useEffect
updates the state of the component, wouldn't it cause the component to re-render? Wouldn't it cause the useEffect to run again, and so on causing an infinite loop? Yes!
Let's see it using an example:
import { useEffect, useState } from "react"
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log("Running useEffect")
setCount(count + 1)
})
console.log("Running render")
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>Click Me</button>
</div>
)
}
export default App
If you open the console, you will see the code is executed indefinitely:
If you look closely, React is showing a warning:
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
This clearly says that you are updating a state inside the useEffect, which is causing the component to re-render.
How to avoid infinite loops and still update the state inside the useEffect?
This is where the dependency array comes into the picture. We will learn about how to use them in the upcoming sections.
Fetching data with useEffect
Let's build a small app where we fetch the bitcoin price and display it. Before implementing the app, let's add some styles to index.css
:
body {
margin: 10px auto;
max-width: 800px;
}
.App {
display: flex;
flex-direction: column;
align-items: center;
}
.refresh {
display: flex;
align-items: center;
}
.refresh-label {
margin-right: 10px;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: 0.4s;
transition: 0.4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: 0.4s;
transition: 0.4s;
}
input:checked + .slider {
background-color: #2196f3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196f3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
We will use the endpoint https://api.coincap.io/v2/assets/bitcoin to fetch the bitcoin price. Now if you are using async-await syntax to fetch the data, your code will look like:
useEffect(async () => {
try {
const response = await fetch("https://api.coincap.io/v2/assets/bitcoin")
const result = await response.json()
const bitcoinPrice = (+result?.data?.priceUsd).toFixed(2)
setPrice(bitcoinPrice)
} catch (error) {
console.log("error", error)
}
}, [])
If you use this code, you will get a warning from React telling us not to make useEffect
callbacks async. How to tackle this problem? The error message itself suggests having another async function and call it inside the useEffect
callback.
So if we update our code accordingly it will look like the following:
import { useEffect, useState } from "react"
function App() {
const [price, setPrice] = useState()
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("https://api.coincap.io/v2/assets/bitcoin")
const result = await response.json()
const bitcoinPrice = (+result?.data?.priceUsd).toFixed(2)
setPrice(bitcoinPrice)
} catch (error) {
console.log("error", error)
}
}
fetchData()
}, [])
return (
<div className="App">
<h2>{price && `Bitcoin Price: $${price}`}</h2>
</div>
)
}
export default App
You might observe that we are passing an empty array as a dependency (the second argument to useEffect
). This is to ensure that the useEffect
runs only once when the component is mounted and not when the component is updated or re-rendered. As you might have guessed correctly, useEffect
with an empty dependency array is the same as that of componentDidMount lifecycle method in a class component.
Now if you run the app, you should be able to see the bitcoin price being displayed:
Running it when certain states change
Since the bitcoin price changes every moment, let's make our app more interesting and fetch the price every 5 seconds!
import { useEffect, useState } from "react"
function App() {
const [price, setPrice] = useState()
useEffect(() => {
let interval
const fetchData = async () => {
try {
const response = await fetch("https://api.coincap.io/v2/assets/bitcoin")
const result = await response.json()
const bitcoinPrice = (+result?.data?.priceUsd).toFixed(2)
setPrice(bitcoinPrice)
} catch (error) {
console.log("error", error)
}
}
fetchData()
interval = setInterval(() => {
fetchData()
}, 5 * 1000)
return () => {
clearInterval(interval)
}
}, [])
return (
<div className="App">
<h2>{price && `Bitcoin Price: $${price}`}</h2>
</div>
)
}
export default App
As you may see, we have added a cleanup call back, which will clear the interval, so that it is cleared before the next render and does not run indefinitely and cause memory leakage. You will find more significance to this in the next section.
Now if you run the app and see the network tab, you will see the call happening every 5 seconds and the price getting refreshed:
Let's not stop here, let's add a toggle button to turn off and on the auto refresh:
import { useEffect, useState } from "react"
function App() {
const [price, setPrice] = useState()
const [autoRefresh, setAutoRefresh] = useState(true)
useEffect(() => {
let interval
const fetchData = async () => {
try {
const response = await fetch("https://api.coincap.io/v2/assets/bitcoin")
const result = await response.json()
const bitcoinPrice = (+result?.data?.priceUsd).toFixed(2)
setPrice(bitcoinPrice)
} catch (error) {
console.log("error", error)
}
}
if (!price) {
// Fetch price for the first time when the app is loaded
fetchData()
}
if (autoRefresh) {
interval = setInterval(() => {
fetchData()
}, 5 * 1000)
}
return () => {
clearInterval(interval)
}
}, [autoRefresh, price])
return (
<div className="App">
<h2>{price && `Bitcoin Price: $${price}`}</h2>
<div className="refresh">
<div className="refresh-label">Auto refresh:</div>
<label className="switch">
<input
type="checkbox"
checked={autoRefresh}
onChange={e => {
setAutoRefresh(e.target.checked)
}}
/>
<span className="slider round"></span>
</label>
</div>
</div>
)
}
export default App
As you can see we have added a state called autoRefresh
, which will be set to true
or false
based on the toggle status of the slider. Also, we have added 2 conditions, one to check if the price is present or not and load the price when it is not present.
Another, to check if the autoRefresh
is enabled, then only run the logic to fetch price every 5 seconds. Since we need useEffect
to be executed every time the value of price
and autoRefresh
changes, we have added it to the dependency array.
The cleanup function will get executed before the next render so that, when we set the autoRefresh
to false
, the interval will be cleared and data will not be fetched any further.
The difference between the cleanup function and componentWillUnmount
is that the cleanup function runs before every re-render and componentWillUnmount
runs only when the whole component is unmounted (towards the end of the component lifecycle). You can read more on why they are different here.
General trivia on useEffect
-
useEffect
needs to be inside the functional component like any other React hook. - One component can have as many
useEffect
as required. React will make sure that they are clubbed together and executed (wherever possible). - Like how state variables can be part of the dependency array, you can have the props as well in the dependency array. Make sure that you are adding only the required dependencies, adding unnecessary dependencies will cause unwanted execution of the effect.
- If you miss adding a dependency, react will show a warning to help you avoid bugs:
Top comments (0)