A Beginner's Guide to the React useEffect Hook
Updated on Jan 14, 2024
Table of Contents
What is a side effect?
A side effect is any operation that affects something outside of the scope of the current function. For example, if you have a function that calculates the sum of two numbers, it does not have any side effects, because it only returns a value based on its inputs and does not modify anything else.
However, if you have a function that fetches some data from an API and updates the state of your component, it does have a side effect, because it changes the state of your component, which in turn affects the UI.
Side effects are not inherently bad, and they are often necessary to make your app interactive and dynamic. However, they need to be handled carefully, because they can introduce bugs, performance issues, or memory leaks if not done correctly.
How to use the useEffect hook
The useEffect hook is a function that takes two arguments: a callback function and an optional dependency array. The callback function is where you write your side effect logic, and the dependency array is where you specify when you want your effect to run.
The basic syntax of the useEffect hook is:
useEffect(
() => {
// your side effect code here
},
[
/* your dependency array here */
]
);
The useEffect hook runs after every render of your component, unless you provide a dependency array. The dependency array is a list of values that determine when your effect should run. If any of the values in the dependency array change, the effect will run again. If the dependency array is empty, the effect will only run once, after the initial render. If the dependency array is omitted, the effect will run after every render.
For example, suppose you have a component that displays a random joke from an API. You want to fetch a new joke every time the user clicks a button. You can use the useEffect hook to fetch the joke and update the state of your component, like this:
import React, { useState, useEffect } from "react";
const Joke = () => {
const [joke, setJoke] = useState("");
const [loading, setLoading] = useState(false);
// fetch a joke from the API
const fetchJoke = async () => {
setLoading(true);
const response = await fetch(
"https://official-joke-api.appspot.com/random_joke"
);
const data = await response.json();
setJoke(data);
setLoading(false);
};
// use the useEffect hook to fetch a joke on the initial render
useEffect(() => {
fetchJoke();
}, []); // empty dependency array means the effect will only run once
// render the joke or a loading message
return (
<div>
{loading ? (
<p>Loading...</p>
) : (
<div>
<p>{joke.setup}</p>
<p>{joke.punchline}</p>
</div>
)}
<button onClick={fetchJoke}>Get another joke</button>
</div>
);
};
export default Joke;
In this example, the useEffect hook has an empty dependency array, which means it will only run once, after the initial render. This way, we can fetch a joke and display it to the user when the component mounts. The user can then click the button to fetch another joke, which will update the state of the component and trigger a re-render.
The dependency array
The dependency array is a very important part of the useEffect hook, because it controls when your effect should run. If you don’t provide a dependency array, or if you provide an incorrect one, you may end up with unwanted or unexpected behavior.
For example, suppose you have a component that displays the current time. You want to update the time every second, so you use the useEffect hook to set up an interval, like this:
import React, { useState, useEffect } from "react";
const Clock = () => {
const [time, setTime] = useState(new Date());
// use the useEffect hook to set up an interval
useEffect(() => {
// create a function to update the time
const tick = () => {
setTime(new Date());
};
// set up an interval to call the tick function every second
const interval = setInterval(tick, 1000);
// return a cleanup function to clear the interval
return () => {
clearInterval(interval);
};
});
// render the time
return <p>{time.toLocaleTimeString()}</p>;
};
export default Clock;
This code seems to work fine at first glance, but there is a subtle problem. The useEffect hook does not have a dependency array, which means it will run after every render of the component. This means that every time the component re-renders, a new interval will be created, and the old one will not be cleared. This will result in multiple intervals running at the same time, which will cause the time to update faster and faster, and eventually crash the app.
To fix this problem, we need to provide a dependency array to the useEffect hook. In this case, we want the effect to run only once, after the initial render, so we can provide an empty dependency array, like this:
useEffect(() => {
// create a function to update the time
const tick = () => {
setTime(new Date());
};
// set up an interval to call the tick function every second
const interval = setInterval(tick, 1000);
// return a cleanup function to clear the interval
return () => {
clearInterval(interval);
};
}, []); // empty dependency array means the effect will only run once
This way, we can ensure that only one interval is created and cleared, and the time will update correctly every second.
The dependency array can also have one or more values, depending on your use case. For example, suppose you have a component that displays a list of posts from an API. You want to fetch the posts based on a category that the user can select from a dropdown menu. You can use the useEffect hook to fetch the posts and update the state of your component, like this:
import React, { useState, useEffect } from "react";
const Posts = () => {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(false);
const [category, setCategory] = useState("all");
// fetch posts from the API based on the category
const fetchPosts = async () => {
setLoading(true);
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?category=${category}`
);
const data = await response.json();
setPosts(data);
setLoading(false);
};
// use the useEffect hook to fetch posts when the category changes
useEffect(() => {
fetchPosts();
}, [category]); // category is a dependency, which means the effect will run whenever the category changes
// handle the change of the category
const handleChange = (event) => {
setCategory(event.target.value);
};
// render the posts or a loading message
return (
<div>
<select value={category} onChange={handleChange}>
<option value="all">All</option>
<option value="business">Business</option>
<option value="technology">Technology</option>
<option value="sports">Sports</option>
</select>
{loading ? (
<p>Loading...</p>
) : (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)}
</div>
);
};
export default Posts;
In this example, the useEffect hook has a dependency array with one value: category. This means that the effect will run whenever the category changes, which happens when the user selects a different option from the dropdown menu. This way, we can fetch the posts that match the selected category and display them to the user.
Cleaning up effects
Some effects may require some cleanup after they are done. For example, if you set up an interval, a subscription, or an event listener in your effect, you need to clear them when your component unmounts, to avoid memory leaks or unwanted behavior.
To perform cleanup in your effect, you can return a function from your callback function. This function will be called when your component unmounts, or before your effect runs again (if you have a dependency array).
For example, in the Clock component that we saw earlier, we returned a function that clears the interval that we set up in our effect, like this:
useEffect(() => {
// create a function to update the time
const tick = () => {
setTime(new Date());
};
// set up an interval to call the tick function every second
const interval = setInterval(tick, 1000);
// return a cleanup function to clear the interval
return () => {
clearInterval(interval);
};
}, []); // empty dependency array means the effect will only run once
This way, we can ensure that the interval is cleared when the component unmounts, and we don’t end up with multiple intervals running at the same time.
Similarly, if you set up an event listener in your effect, you should remove it in your cleanup function, like this:
useEffect(() => {
// create a function to handle the window resize event
const handleResize = () => {
setWidth(window.innerWidth);
};
// add the event listener to the window object
window.addEventListener("resize", handleResize);
// return a cleanup function to remove the event listener
return () => {
window.removeEventListener("resize", handleResize);
};
}, []); // empty dependency array means the effect will only run once
Custom hooks with useEffect
One of the benefits of using hooks is that you can create your own custom hooks to encapsulate and reuse logic across your components. Custom hooks are functions that start with the word “use” and can call other hooks inside them.
You can use the useEffect hook inside your custom hooks to create reusable effects for your components. For example, suppose you want to create a custom hook that fetches data from an API and returns the data, the loading state, and the error state. You can use the useEffect hook to fetch the data and update the state of your custom hook, like this:
import { useState, useEffect } from "react";
// create a custom hook that takes a URL as an argument
const useFetch = (url) => {
// create a state for the data, the loading state, and the error state
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// use the useEffect hook to fetch the data from the URL
useEffect(() => {
// set the loading state to true
setLoading(true);
// use the fetch API to get the data
fetch(url)
.then((response) => {
// check if the response is ok
if (response.ok) {
// return the response as JSON
return response.json();
} else {
// throw an error with the status text
throw new Error(response.statusText);
}
})
.then((data) => {
// set the data state with the fetched data
setData(data);
// set the loading state to false
setLoading(false);
})
.catch((error) => {
// set the error state with the error message
setError(error.message);
// set the loading state to false
setLoading(false);
});
}, [url]); // use the URL as a dependency, so the effect will run whenever the URL changes
// return the data, the loading state, and the error state from the custom hook
return [data, loading, error];
};
export default useFetch;
Now, you can use this custom hook in any component that needs to fetch data from an API, like this:
import React from "react";
import useFetch from "./useFetch";
const Users = () => {
// use the custom hook to fetch the users data from an API
const [users, loading, error] = useFetch(
"https://jsonplaceholder.typicode.com/users"
);
// render the users data or a loading or error message
return (
<div>
{loading ? (
<p>Loading...</p>
) : error ? (
<p>Error: {error}</p>
) : (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
};
export default Users;
This way, you can abstract and reuse the logic of fetching data and handling the loading and error states in your custom hook, and use it in any component that needs it.
Common useEffect pitfalls and how to avoid them
The useEffect hook can be tricky to use at first, and it may cause some unexpected or undesired behavior if not used correctly. Here are some common pitfalls that you may encounter when using the useEffect hook, and how to avoid them.
Pitfall 1: Missing dependencies
One of the most common pitfalls when using the useEffect hook is forgetting to include some dependencies in the dependency array. This can cause your effect to run with stale or outdated values, and lead to bugs or inconsistencies in your app.
For example, suppose you have a component that displays a counter and a button that increments the counter. You also want to log the counter value to the console every time it changes. You may write something like this:
import React, { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
// use the useEffect hook to log the count value to the console
useEffect(() => {
console.log(`The count is ${count}`);
}, []); // empty dependency array means the effect will only run once
// handle the click of the button
const handleClick = () => {
setCount(count + 1);
};
// render the counter and the button
return (
<div>
<p>The count is {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
};
export default Counter;
In this example, the useEffect hook has an empty dependency array, which means it will only run once, after the initial render. However, this means that the effect will always use the initial value of count, which is 0, and will not reflect the updated value of count when the button is clicked. This will result in the console logging “The count is 0” every time the component re-renders, instead of the actual count value.
To fix this problem, we need to include count as a dependency in the dependency array, like this:
useEffect(() => {
console.log(`The count is ${count}`);
}, [count]); // count is a dependency, which means the effect will run whenever the count changes
This way, we can ensure that the effect will use the latest value of count, and will log the correct value to the console every time the count changes.
A good way to avoid missing dependencies is to use a linter plugin, such as eslint-plugin-react-hooks, which can warn you if you have any missing or unnecessary dependencies in your useEffect hook.
Pitfall 2: Infinite loops
Another common pitfall when using the useEffect hook is creating an infinite loop, which can cause your app to crash or freeze. This can happen when your effect updates a state or a prop that is also a dependency of your effect, causing the effect to run again and again.
For example, suppose you have a component that fetches some data from an API and updates the state of your component with the data. You may write something like this:
import React, { useState, useEffect } from "react";
const Data = () => {
const [data, setData] = useState(null);
// use the useEffect hook to fetch the data from the API
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((data) => {
// update the data state with the fetched data
setData(data);
});
}, [data]); // data is a dependency, which means the effect will run whenever the data changes
// render the data or a loading message
return <div>{data ? <p>{data.title}</p> : <p>Loading...</p>}</div>;
};
export default Data;
In this example, the useEffect hook has a dependency array with one value: data. This means that the effect will run whenever the data changes, which happens when the effect fetches the data and updates the state. However, this also means that the effect will run again after the state is updated, causing an infinite loop of fetching and updating data.
To fix this problem, we need to remove data from the dependency array, since we don't want the effect to run when the data changes. We only want the effect to run once, after the initial render, to fetch the data. We can do so by providing an empty dependency array, like this:
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((data) => {
// update the data state with the fetched data
setData(data);
});
}, []); // empty dependency array means the effect will only run once
This way, we can avoid the infinite loop of fetching and updating data, and display the data correctly to the user.
A good way to avoid infinite loops is to be careful about what you include in the dependency array, and avoid updating any state or prop that is also a dependency of your effect.
Pitfall 3: Unnecessary re-renders
Another common pitfall when using the useEffect hook is causing unnecessary re-renders of your component, which can affect the performance and user experience of your app. This can happen when your effect updates a state or a prop that is not used in your component, or when your effect runs too often due to an incorrect dependency array.
For example, suppose you have a component that displays a random quote from an API. You want to fetch a new quote every 10 seconds, so you use the useEffect hook to set up an interval, like this:
import React, { useState, useEffect } from "react";
const Quote = () => {
const [quote, setQuote] = useState("");
const [loading, setLoading] = useState(false);
// fetch a quote from the API
const fetchQuote = async () => {
setLoading(true);
const response = await fetch("https://api.quotable.io/random");
const data = await response.json();
setQuote(data.content);
setLoading(false);
};
// use the useEffect hook to set up an interval
useEffect(() => {
// fetch a quote on the initial render
fetchQuote();
// set up an interval to fetch a new quote every 10 seconds
const interval = setInterval(fetchQuote, 10000);
// return a cleanup function to clear the interval
return () => {
clearInterval(interval);
};
}, []); // empty dependency array means the effect will only run once
// render the quote or a loading message
return <div>{loading ? <p>Loading...</p> : <p>{quote}</p>}</div>;
};
export default Quote;
This code seems to work fine, but there is a subtle problem. The useEffect hook updates two states: quote and loading. However, the loading state is not used in the component, and it does not affect the UI. This means that every time the loading state changes, the component will re-render unnecessarily, which can cause performance issues or flickering.
To fix this problem, we need to remove the loading state from the component, and only update the quote state, which is used in the component and affects the UI. We can do so by simplifying the fetchQuote function, like this:
// fetch a quote from the API
const fetchQuote = async () => {
const response = await fetch("https://api.quotable.io/random");
const data = await response.json();
setQuote(data.content);
};
This way, we can avoid the unnecessary re-renders of the component, and only update the UI when the quote changes.
A good way to avoid unnecessary re-renders is to use the React DevTools to inspect your component and see what causes it to re-render, and to only update the state or props that are used in your component and affect the UI.
Conclusion
The useEffect hook is a powerful and versatile tool that allows you to perform side effects in your functional components. However, it also requires some careful attention and understanding to use it correctly and avoid common pitfalls.
In this blog post, we learned the basics of the useEffect hook, how it differs from the class-based lifecycle methods, and how to use it effectively in your React projects. We also learned how to create custom hooks with useEffect, and how to avoid some common pitfalls such as missing dependencies, infinite loops, and unnecessary re-renders.
How did you like the article?
Thanks for reading! 🙏