React's useEffect
hook is one of the trickiest and most powerful hooks in functional components, as it allows you to perform side effects in react. But what is a side effect? In React, a ‘side effect’ is any operation that affects something outside of the component such as making a REST API call, updating the DOM, etc. It will run after the component is rendered. If you are familiar with class-based components, then the useEffect hook can be easily understood as a combination of the lifecycle methods componentDidMount, componentDidUpdate, and componentWillUnmount.
In this article, we will cover the usage of the useEffect Hook in detail and with examples.
The useEffect hook should be used anytime you need your functional component to perform a side effect. This can include things like fetching data, setting up subscriptions, and updating the DOM. It is important to note that the useEffect hook should not be used for rendering purposes, as it is not designed to replace React's rendering mechanism.
Some of the scenarios where the useEffect hook can come in handy include:
The syntax of the useEffect hook is as follows,
useEffect(() => {
// function body
}, [dependencies]);
The useEffect
hook is called within a functional component and it takes two arguments: a function that represents the effect body and an optional array of dependencies. The effect function is executed after the component has been rendered. When the dependencies array is specified, and the values of the arguments in the dependencies array are changed, it will trigger to re-run the effect.
Below is the syntax of the useEffect hook with a cleanup function.
useEffect(() => {
// effect function
return () => {
// cleanup function
};
}, [dependencies]);
The effect function can return a cleanup
function that will be run before the effect is run again or before the component unmounts. This cleanup function can be used to perform any necessary cleanup operations, such as canceling network requests or removing event listeners or unsubscribing from data sources, etc.,
There can be more than one useEffect in the same functional component.
To use the useEffect hook, you will first need to import it from the react
library. Then, you can call the useEffect function within your component, and pass in a function that represents the effect you want performed.
import { useEffect } from "react";
function MyComponent() {
useEffect(() => {
// Your effect function here
}, []);
return <div>Hello World</div>;
}
Let's see the detailed usage of useEffect with examples,
When the dependency array is not specified at all, then the useEffect will be executed every time the component renders.
import { useEffect } from "react";
function MyComponent() {
useEffect(() => {
console.log("This will be run every time the component renders");
});
return <div>Hello World</div>;
}
This case is uncommon and usually, we wouldn't use this scenario in real-time applications.
When an empty dependency array is passed, the useEffect hook will be executed only once when the component mounts into the DOM. Let's say we need to fetch the blog posts of the author once they log in. In this scenario, it's enough to fetch the blog posts only once instead of fetching them every time the component re-renders.
import { useEffect, useState } from "react";
function Posts() {
const [posts, setposts] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users/1/posts")
.then((resp) => resp.json())
.then((blogPosts) => setposts(blogPosts));
}, []);
return (
<div className="App">
{posts && posts.map((post) => <li>{post.title}</li>)}
</div>
);
}
export default Posts;
In the above example, we are fetching the posts of a user only once and rendering it to the DOM only once.
Some other scenarios where you will pass an empty dependency array.
When an argument is passed in the dependency array, it ensures that the effect is re-run whenever its value changes.
Let's say we need to implement a search feature that filters articles/blog posts based on the keyword entered by the user. In that case, we can pass the search keyword as an argument and implement the filter logic in the effect body.
import { useEffect, useState } from "react";
function Search() {
const [posts, setposts] = useState([]);
const [search, setsearch] = useState("");
useEffect(() => {
const filteredPosts = posts.filter((p) => p.title.includes(search));
setposts(filteredPosts);
}, [search]);
return (
<div className="App">
{posts && (
<input
type="text"
value={search}
onChange={(e) => setsearch(e.target.value)}
/>
)}
{posts && posts.map((post) => <li>{post.title}</li>)}
</div>
);
}
export default Search;
So, whenever the user enters a search term, the search
state changes and causes the effect to re-run.
In all the above examples, we have not used the optional cleanup function. But there will be a few cases where we might need to use the cleanup function.
Let's say we need to implement a scenario where, when the user clicks on the button it displays the dropdown. And when the user clicks anywhere outside the dropdown, it should close the dropdown automatically. To achieve this, we can use event listeners.
import { useEffect, useRef, useState } from "react";
function Dropdown() {
const ref = useRef(null);
const [open, setOpen] = useState(false);
const [options, setoptions] = useState([
{ key: 1, value: "Audi" },
{ key: 2, value: "BMW" },
{ key: 3, value: "Jaguar" },
{ key: 4, value: "Ferrari" }
]);
const [option, setOption] = useState("");
useEffect(() => {
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener("click", handleClickOutside);
return () => document.removeEventListener("click", handleClickOutside);
}, []);
return (
<div ref={ref}>
<button onClick={() => setOpen(!open)}>Toggle Dropdown</button>
{open && (
<ul>
{options.map((option) => (
<li key={option.key} onClick={() => setOption(option.value)}>
{option.value}
</li>
))}
</ul>
)}
</div>
);
}
export default Dropdown;
In this example, we have set up a DOM event listener that closes the dropdown when the user clicks outside the dropdown item. The empty dependencies array ensures that the effect is only run once, on-mount, and the cleanup function is used to remove the event listener when the component unmounts.
Some other scenarios when you want to implement the cleanup function.
In the previous section, we saw various examples of using the useEffect hook. In this section, we will see "How not to use it", i.e., the common mistakes that developers make when using the useEffect hook.
import { useEffect, useState } from "react";
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
return <div>{count}</div>;
}
In this example, the useEffect hook is called without a dependencies array, causing the effect function to be executed on every render. This results in an infinite loop, as the effect function updates the count state, causing the component to re-render and the effect to run again.
If you don't include the empty dependency array altogether when necessary, the useEffect will be re-run on every render, which could lead to performance issues in your application.
For example, consider the same example that we used in Example 2 of the previous section, but without passing the dependency array
import { useEffect, useState } from "react";
function Posts() {
const [posts, setposts] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users/1/posts")
.then((resp) => resp.json())
.then((blogPosts) => setposts(blogPosts));
});
return (
<div className="App">
{posts && posts.map((post) => <li>{post.title}</li>)}
</div>
);
}
export default Posts;
So, In this case, every time the component renders, an API call will be made to fetch the data from the backend API, which is unnecessary and consumes extra network traffic, thereby affecting the performance of the application.
If you include unnecessary dependencies in the dependencies array of the useEffect hook, the effect will be re-run unnecessarily and could potentially cause performance issues in your application.
import { useEffect } from "react";
function TodoList({ todos, filter }) {
useEffect(() => {
console.log("filtering todos");
// filter todos
}, [todos, filter]);
return <div>{/* todo list JSX */}</div>;
}
In the above example, the useEffect hook is set up to filter the todos array when the todos or filter props change. However, the filter prop is not used in the effect and therefore should not be included in the dependencies array. This could lead to the effect being re-run unnecessarily when the filter prop changes.
If you don't include a cleanup function in the useEffect hook but you set up any resources that need to be cleaned up (e.g., DOM event listeners, intervals, socket connections, etc.), it would result in memory leaks and performance problems.
For example, consider the same scenario that we used in Example 4 of the previous section, but without using a cleanup function.
import { useEffect, useRef, useState } from "react";
function Dropdown() {
const ref = useRef(null);
const [open, setOpen] = useState(false);
const [options, setoptions] = useState([
{ key: 1, value: "Audi" },
{ key: 2, value: "BMW" },
{ key: 3, value: "Jaguar" },
{ key: 4, value: "Ferrari" }
]);
const [option, setOption] = useState("");
useEffect(() => {
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener("click", handleClickOutside);
// No Cleanup function
}, []);
return (
<div ref={ref}>
<button onClick={() => setOpen(!open)}>Toggle Dropdown</button>
{open && (
<ul>
{options.map((option) => (
<li key={option.key} onClick={() => setOption(option.value)}>
{option.value}
</li>
))}
</ul>
)}
</div>
);
}
export default Dropdown;
If we don't include a cleanup function, then the DOM event listener that we created in the effect body will not be removed when the component unmounts.
If the event listener is not removed when the component unmounts, it will continue to listen for clicks on the document, even if the component is no longer rendered. This can lead to memory leaks, as the event listener will continue to consume resources even if it is no longer needed. So, it is always necessary to include a cleanup function in the useEffect that removes any DOM event listeners when the component unmounts. This will ensure that the event listeners are cleaned up properly and that resources are released when they are no longer needed.
In this article, we have explored the usage of useEffect and we have seen examples of How to/How not to use the Effect hook. In conclusion, the useEffect hook is a powerful tool in React that allows you to perform side effects in function components. It is important to use the useEffect hook correctly to avoid performance issues. By following the best practices and avoiding the common mistakes as explained in the article, you can effectively manage side effects in your React projects.
Also published here.