Have you ever faced this issue that your useEffect
hook gets executed multiple times? Maybe sometimes it goes into the infinite loop?
When React released the hooks feature, everyone jumped into it and started exploring new features and use-cases. I also started using functional components and completely abandoned the “class components” in my new projects.
In all of my projects, I was using some backend APIs and I was quite comfortable with componentDidMount
. Because it was calling the APIs only once. And even after updating the redux store or state, it wasn’t executing the same code inside componentDidMount
again unlike the useEffect()
. So I decided to check how useEffect works. After reading the official docs properly, I got some amazing results. And I’m using them for almost a year now.
useEffect() basics
In this section, I won’t dive too deep into the basics because there are a lot of blogs and video tutorials out there that explain everything. Rather, we’ll focus on how to use this properly and avoid unnecessary function calls and re-renders. So, let’s start with the simple code.
If you run this code, you can see that the useEffect hook will be called only after executing all the code inside our component. The class equivalent code of this snippet would be something like this:
import React from 'react';
class App extends React.Component {
componentDidMount() {
console.log('Hello from useEffect!');
}
componentDidUpdate() {
console.log('Hello from useEffect!');
}
render() {
console.log('Hello from the Component!');
return (
<div>Hello World</div>
);
}
}
export default App;
Now, as you all know, if we try to manipulate the state or redux store, it re-renders the component. It means this useEffect
hook will also be executed after updating the state or props. If you’re updating the state or props inside this hook, it would simply go into an infinite loop because of this.
// Infinite loop alert!
import React, { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Hello from useEffect!');
setCount(count + 1);
});
console.log('Hello from the component!');
return (
<div>Hello World</div>
);
}
export default App;
How to prevent this infinite loop?
When I shifted from Class components to Functional components, I didn’t know that the useEffect
hook also accepts the second parameter. And this parameter is somewhat responsible for controlling when this hook gets executed.
So what is this second parameter?
This second parameter accepts an array. We can provide different variables, i.e. the variables created by useState
or redux
from props
in this array. If we pass an empty array []
, it just renders the component only once like componentDidMount
. Let’s look at this example:
import React, { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Hello from useEffect!');
}, []);
console.log('Hello from the component!');
if(count < 1) {
setTimeout(() => {
setCount(count + 1);
}, 1000);
}
return (
<div>Hello World</div>
);
}
export default App;
In this code, the code inside useEffect
will only get executed once even if we change the value of the variable count
multiple times. It’d print “Hello from the component!” twice but it’d print “Hello from useEffect” only once.
If you’ve noticed, I’m increasing the value of count
only once using the if condition. Because after passing the second argument, it won’t execute useEffect
hook again but it’ll try to re-render the component by executing the other code. You can try changing the value in if condition to verify whatever we’ve done so far.
In the beginning, we saw that the class equivalent code of useEffect
was also executing the code after the component finishes re-rendering. And it is also possible to re-render a component only on a specific condition in the class components. So there must be a way to do this using useEffect
.
How to conditionally execute useEffect?
Let’s take the same example. Now we’ll modify the array we passed in the second argument. If I want to execute the useEffect
hook again only if the value of count
changes, I can pass the count
in the array like this:
import React, { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Hello from useEffect!');
}, [count]); // <-- See the change here
console.log('Hello from the component!');
if(count < 1) {
setTimeout(() => {
setCount(count + 1);
}, 1000);
}
return (
<div>Hello World</div>
);
}
export default App;
This code will execute useEffect
hook after the first component render just like componentDidMount
. And after that, it’ll only get executed if we change the value of count somehow. This is not only valid for the variables we create using useState. We can also use them for the different props be it coming from redux or some value from the parent component.
How to pass props in this array?
It is pretty simple and straight forward to use different props a component is getting from either redux or the parent component.
E.g. if we’re getting the email from the props as props.email
, we can simply write our array like this: [props.email]
. We can also pass multiple variables in this array: [count, props.email, props.name]
. In this case, it’ll execute useEffect
whenever the value of either count, email or name gets changed.
I wrote this blog to show why useEffect
hook gets executed multiple times and how to prevent this by passing the second argument. I had discussed about the best practices too many times with a lot of people in my organisation and also in a lot of meetups/conferences but we never concluded anything unanimously. But I would love to know your thoughts. You can comment here or simply DM me on Twitter.
Cover Image source: https://hackernoon.com/react-hooks-63192d1d