React: Master useEffect Hook.
A complete guide for absolute beginners.
Introduction
useEffect()
is one of the most used hooks in React applications. This makes it a favorite topic for interviewers to talk about it in Frontend/React interviews. Therefore having a good understanding of useEffect
is crucial.
You might be knowing, that we can write React applications either using class-based components or using functional components (using hooks).
In this article, I don't assume you have knowledge of writing class-based components. Though we will see useEffect
in context with class-based components in a later section of the article.
Let's Begin!
Use and Syntax
useEffect
is used to cause side effects
, for example, making an API request, manipulating DOM directly, adding timers (setTimeout or setInterval) etc.
Syntax
useEffect(() => {
// callback function
return () => {
// cleanup
}
}, [dependencies]);
Let us understand the syntax
The useEffect
hook takes a callback function as its first argument and optionally an array of variables
( referred to as dependency array) as the 2nd argument. The variables could be anything, a primitive type or a reference type.
useEffect
can optionally return a function too. We will see the significance of all these things involved in the syntax. ( Have some patience) ๐
.
Million dollar question is - When actually useEffect
runs?
You have written the code inside usEffect
( Inside the useEffect
's callback, to be precise), but when does that code run?
The callback function passed to useEffect
is executed after every render by default. I said by default, meaning there are ways to control it. We will look into that as well.
Let's take an example and see that useEffect
runs after every render.
import { useEffect } from "react";
function App() {
useEffect(() => {
console.log("UseEffect fired");
});
// This console just before render
console.log("Just before render");
return (
<div>
<h1>Example - UseEffect</h1>
</div>
);
}
export default App;
Start the React app, navigate to the browser, and check the console logs.
Notice that console.log
written just before the return statement is printed first and then the console.log
written inside the useEffect
is printed. This proves our point that useEffect
runs after render.
Now, let's add a state variable count
and a button to update that count
when clicked. You should be knowing that whenever the state or props of a component changes, it is re-rendered.
We are doing this to prove that useEffect
runs after every render. ๐
import { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("UseEffect fired");
});
// This console just before render
console.log("Just before render");
return (
<div>
<h3> Count - {count}</h3>
<button onClick={() => setCount(count + 1)}> Click me</button>
</div>
);
}
export default App;
Go and test this. When you click the Click me
button, it increases the count
that causes the app to re-render.
As you can see in the image below, console.log
inside useEffect
is printed 4 times, the first print is when the app was rendered for the first time, and then 3 more times as I have clicked the button 3 times to increase the count to 3.
So, we can conclude that useEffect
runs after every render.
A common mistake beginners make -
Think about you have a boolean state variable and you set the value of that to true/false
inside useEffect
. If it is true
you set it to false
and vice-versa. Let's write code to do this.
import { useState, useEffect } from "react";
function App() {
const [bool, setBool] = useState(true);
useEffect(() => {
setBool(!bool);
});
// This console just before render
console.log(bool);
return (
<div>
<h3>Beginners mistake</h3>
</div>
);
}
export default App;
Try to think, what would happen if you run this code? It might freeze your browser (frozen mine lol ๐).
You can see the error in the image below. This is basically a StackOverflow error, meaning you keep calling useEffect
infinitely.
But, how does it happen? Let's see that next.
The reason for this error is that there is an infinite cycle of re-renders in our app.
Identify The Cycle -
Component rendered for the first time -> useEffect
runs and changes the state variable -> As state got changed, component re-rendered -> useEffect
fired and changed the state variable again -> As state got changed, component re-rendered -> useEffect
fired and changed the state variable again -> As state got changed, component re-rendered -> useEffect
fired.............so on.
I can keep writing this infinitely ๐.
How can we stop it and make it work properly?
The answer is to make use of the 2nd argument passed to useEffect
, the dependency array.
You can put as many variables ( functions, objects, or primitive type variables) as you wish inside that array. Doing so is basically telling React to run useEffect
whenever any of the variables mentioned in the dependency array changes.
Let's see a couple of examples.
Example -1
import { useState, useEffect } from "react";
function App() {
const [count1, setCount1] = useState(10);
const [count2, setCount2] = useState(20);
useEffect(() => {
console.log("Useffect executed");
}, [count2]);
return (
<div>
<h3>Count1: {count1}</h3>
<h3>Count2: {count2}</h3>
<button onClick={() => setCount1(count1 + 1)}>Change counter -1 </button>
<button onClick={() => setCount2(count2 + 1)}>Change counter -2 </button>
</div>
);
}
export default App;
Look at the above code closely. Think about -
- What would happen when you run this code?
- What happens when you click the
Change counter -1
button? - What happens when you click the
Change counter -2
button?
In the example-1 above, useEffect
runs -
- When the component is rendered for the first time.
useEffect
is always executed after the first render. Nothing interesting here. useEffect
will be fired every time the value of thecount2
changes. Why? Because we have includedcount2
in the dependency array and as I told youuseEffect
runs whenever any of the variables mentioned in the dependency array changes.
Here is what is happening and why?
- When
Change counter -1
is clicked, thecount1
changes and the component is re-rendered. Although, the component is re-rendereduseEffect
doesn't execute because the dependency array doesn't include the changed variablecount1
. - When
Change counter -2
is clicked, thecount2
changes, and the component is re-rendered. Then,useEffect
is executed because the dependency array includes the changed variablecount2
.
You can play with all the examples here.
Example -2 Now, change our effect to something like this ๐
// Example -2
useEffect(() => {
console.log("Useffect executed", count1, count2);
}, [count2, count1]);
In this example-2
, clicking on any of the buttons changes either count1
or count2
and change in anyone of these two triggers to run useEffect
because both are included in the dependency array.
Hope things are making sense to you. If not, reach out to me here on Twitter
Example -3
What if you want your useEffect
to run only once? It is simple, just tell React that this useEffect
doesn't depend on anything.
How to tell that, by passing an empty array as the 2nd argument to useEffect
.
// Example -3
useEffect(() => {
console.log("This Useffect executed only once, just after first rended");
}, [ ]);
Looking at the empty dependency array, React concludes that there is nothing changed in the dependency array so it decides not to execute useEffect
.
Multiple useEffect
in an Component
Yes, we can have more than one useEffect
hook inside our components. This is very useful for separating logic and controlling when to run the useEffect
.
For example, think about the situation when we want to run a different piece of code when count1
changes and a completely different piece of code when count2
changes.
useEffect(() => {
console.log("Useffect executed after change in count1");
}, [count1]);
// // Example -3
useEffect(() => {
console.log("Useffect executed after change in count2");
}, [ count2 ]);
The first useEffect
runs whenever count1
changes and the second useEffect
runs when count2
changes. Both of the effects run after the first render though.
Data Fetching From an API Endpoint.
As I told you, useEffect
is used to cause side effects
, for example, making an API request. So, let's make a request to the API. I will be using this https://forkify-api.herokuapp.com/api/get?rId=47746
that sends the details of a dish.
We will store these details in a state variable and show them on our web page.
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [loading, setLoading] = useState(true);
const [dish, setDish] = useState({
title: "",
image_url: ""
});
useEffect(() => {
fetch("https://forkify-api.herokuapp.com/api/get?rId=47746")
.then((res) => res.json())
.then((dish) => {
const { title, image_url } = dish.recipe;
console.log(dish);
setDish({
title,
image_url
});
setLoading(false);
});
}, []);
console.log(dish);
return (
<div className="App">
{loading ? (
<h2>Loading....</h2>
) : (
<div>
<h1> {dish.title} </h1>
<img src={dish.image_url} alt="Pizza" />
</div>
)}
</div>
);
}
Play with the code here
When to return a cleanup
function from useEffect
.
As discussed in the beginning, we can optionally return a function from useEffect
. This returned function is generally termed as cleanup
. Let's talk about this.
When we return a function from inside the callback of the useEffect
, this function is executed by React just before unmounting the component and also after every re-render. In simple terms, Unmounting means the removal of a component from the user interface.
Read more about why cleanups run after every re-renderhere.
We use this function to do cleanup
like removing eventListeners
clearing timers using clearInterval
clearTimeout
. And, doing this is actually necessary and very logical.
Think about if a component is removed why should we let eventListeners
attached to it and put the burden on the browser. This can even cause memory leaks. So to avoid this, we make use of the ability to return a cleanup function from inside useEffect
.
Let's write some code.
useEffect(() => {
const id = setTimeout(() => {
console.log("Settimeout called after 1000 ms")
});
// This is cleanup function
// You can write es6 arrow function too if u wish
return function(){
clearTimeout(id)
}
}, [ ]);
Explanation: Inside useEffect
we have used setTimeout
. At the time of unmounting, we should clear this timeout
.
To do this, we are returning a function from useEffect
. React executes this returned function at the time of unmounting the component. As soon as React executes this function, clearTimeout(id)
s expected and this clears the used setTimeout.
Relaton with class-based components.
This section is only for those who have prior knowledge of writing class-based components.
useEffect
basically serves the purpose of 3 lifecycle methods used in class-based components -
- componentDidMount();
- componentDidUpdate();
componentWillUnmount(); Here is how -
When
useEffect
has an empty dependency array, it is executed only once, after the component mounts. This is same as howcomponentDidMount()
works.- When
useEffect
has a non-empty dependency array, it is executed every time any of the variable inside the dependency array is updated. This is same as howcomponentDidUpdatet()
works. - React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once.
This is why React also cleans up effects from the previous render before running the effects next time. This serves the same purpose as componentWillUnmount()
but it does more than that.
This is all I wanted to share with you in this article. I strongly hope you find this article helpful in your learning journey. If you get any question to ask, please reach out to me on Twitter here.
Don't forget to like this article and give your valuable feedback in the comments. I'm waiting to hear your thoughts on what you have to say about the article.
You can follow me here and on Twitter to learn more in-depth articles on various JavaScript and React topics.
Keep learning. Thank you!