Certain actions happen outside of the scope of a component, such as fetching data from external API, reading or writing from the file system, modifying the DOM tree manually, and so on. These actions are referred to as side effects.
useEffect()
is a React hook that allows you to write side effects inside React components, providing a portal to the outside of the application. It is most commonly used to synchronize the component with external systems.
Basic syntax
The useEffect()
hook accepts a callback function. By default, this function will be executed after every render.
1useEffect(() => {
2 // Perform some action here
3});
This default behavior may lead to problems, especially when the side effect is resource-intensive, and you want to avoid unnecessary reruns. In this case, you can modify the default behavior by defining a dependency array.
If an empty array is provided, the function will be executed only once after the component is mounted (when the component first appears).
1useEffect(() => {
2 // Perform some action here
3}, []);
However, if you specify dependencies in the array, which are usually reactive variables such as props and states, the function will re-execute when the dependencies change.
1useEffect(() => {
2 // Perform some action here
3}, [count]); // The callback function will be executed every time "count" changes
And lastly, the callback function inside useEffect()
could return another function, which will be executed when the component unmounts. This function usually contains cleanup code for the side effect.
1useEffect(() => {
2 // Perform some action here
3 return () => {
4 // Clean up code
5 };
6}, [count]); // The callback function will be executed every time "count" changes
Getting started with effects
As an example, let's create a user profile page that fetches data from an external API.
1import { useState, useEffect } from "react";
2
3export default function App() {
4 const [user, setUser] = useState([]);
5 const [loading, setLoading] = useState(true);
6
7 useEffect(() => {
8 async function fetchUser() {
9 const response = await fetch("https://randomuser.me/api/");
10 const data = await response.json();
11 setUser(data.results[0]);
12 setLoading(false);
13 }
14
15 fetchUser();
16 });
17
18 if (loading) return <div>Loading user...</div>;
19
20 return (
21 <div>
22 <h1>
23 {user.name.first} {user.name.last}
24 </h1>
25 <p>Email: {user.email}</p>
26 <img src={user.picture.large} alt="User profile" />
27 </div>
28 );
29}
We defined two states in this example, user
and loading
. user
is used to store user data, which will be retrieved from an external API later.
And since retrieving data might be time consuming, we are also using loading
to indicate whether the data is still being fetched. loading
will be initialize as true
, and after the data has been retrieved, it should be set to false
.
The effects will be executed after the component has been rendered, so React will jump ahead and render the component first. This is why, for a split second, you will see Loading user...
being rendered.
But right after that, React will start executing the side effect.
1useEffect(() => {
2 async function fetchUser() {
3 const response = await fetch("https://randomuser.me/api/");
4 const data = await response.json();
5 setUser(data.results[0]);
6 setLoading(false);
7 }
8
9 fetchUser();
10});
In this case, we are retrieving data from https://randomuser.me/api/
, and use the returned data to update user
. And after user
has been updated, loading
will be set to false
.
However, there is a problem with our code. If you execute the above demo, you will see the user data being updated repeatedly. This is because, by default, the effect will be executed after every render.
And in the above example, we are updating states in the effect, which will trigger a rerender right after, causing the app to be stuck inside a loop.
To fix this, you need to define an empty array of dependencies like this:
1useEffect(() => {
2 async function fetchUser() {
3 const response = await fetch("https://randomuser.me/api/");
4 const data = await response.json();
5 setUser(data.results[0]);
6 setLoading(false);
7 }
8
9 fetchUser();
10}, []); // Add an empty array of dependencies
And this time, the effect will be executed only once after the component is mounted (when the component first appears).
Adding dependencies
Now we have two options, either run the effect for every render, or run it once for the first mount. But in most cases, you need something more flexible.
For instance, you have a list of articles that should update whenever the user selects a different user ID, and you need to fetch new data based on changing user selections.
To handle such use cases, you can specify certain state variables as dependencies. This way, the effect will rerun only when those specific states change.
Let's take a look at this example:
1import { useState, useEffect } from "react";
2
3export default function App() {
4 const [userId, setUserId] = useState("1");
5 const [articles, setArticles] = useState([]);
6 const [loading, setLoading] = useState(true);
7
8 useEffect(() => {
9 async function fetchArticles() {
10 setLoading(true);
11 try {
12 const response = await fetch(
13 `https://jsonplaceholder.typicode.com/posts?userId=${userId}`
14 );
15 const data = await response.json();
16 setArticles(data || []);
17 } catch (error) {
18 console.error("Error fetching articles:", error);
19 }
20 setLoading(false);
21 }
22
23 fetchArticles();
24 }, [userId]); // Re-run effect when the userId changes
25
26 if (loading) return <div>Loading articles...</div>;
27
28 return (
29 <div>
30 <h1>Articles for User {userId}</h1>
31
32 <select value={userId} onChange={(e) => setUserId(e.target.value)}>
33 <option value="1">User 1</option>
34 <option value="2">User 2</option>
35 <option value="3">User 3</option>
36 <option value="4">User 4</option>
37 <option value="5">User 5</option>
38 </select>
39
40 <ul>
41 {articles.map((article) => (
42 <li key={article.id}>
43 <h2>{article.title}</h2>
44 <p>{article.body}</p>
45 </li>
46 ))}
47 </ul>
48 </div>
49 );
50}
In this case, the side effect will be dependent on the state userId
. When userId
changes, the side effect will be re-executed.
Also, notice that it is OK if a listed dependency is not used in the effect, but the reverse is not true. Reactive variables (props and states) used inside the effect must be listed as dependency, unless they are defined inside that effect.
React enforces this to prevent unwanted errors. If a reactive variable is used but not listed as a dependency, the useEffect()
hook will not update properly when that variable changes, leading to unexpected results.
Clean up code
Optionally, the side effect could return a cleanup function, which will be executed when the component unmounts. For instance, let's modify our user profile example, and add a button to control if the user profile should be mounted.
App.jsx
1import { useState, useEffect } from "react";
2import User from "../components/User";
3
4export default function App() {
5 const [mount, setMount] = useState(true);
6
7 return (
8 <div>
9 {mount ? <User /> : <p>User component is unmounted.</p>}
10 <button
11 onClick={() => {
12 setMount(!mount);
13 }}>
14 Toggle User
15 </button>
16 </div>
17 );
18}
components/User.jsx
1function User() {
2 const [user, setUser] = useState([]);
3 const [loading, setLoading] = useState(true);
4
5 useEffect(() => {
6 async function fetchUser() {
7 const response = await fetch("https://randomuser.me/api/");
8 const data = await response.json();
9 setUser(data.results[0]);
10 setLoading(false);
11 }
12
13 fetchUser();
14
15 return () => {
16 console.log("User component is unmounted.");
17 };
18 }, []);
19
20 if (loading) return <div>Loading user...</div>;
21
22 return (
23 <>
24 <h1>
25 {user.name.first} {user.name.last}
26 </h1>
27 <p>Email: {user.email}</p>
28 <img src={user.picture.large} alt="User profile" />
29 </>
30 );
31}
Click the button to unmount the User
component, and you should see the following log in the browser console.
1User component is unmounted.
Of course, this example is only for demonstration, we try to keep the code as simple as possible so that you can see the function being executed when the component unmounts, but in practice, the cleanup code should perform some actual cleanup actions.
For example, imagine you are creating a chat app where you need to connect to a WebSocket server to receive live updates.
The WebSocket connection constantly consumes system resources, as it needs to listen to incoming messages. When the app unmounts, it is necessary to close the connection to free the resources. Failing to do so would waste memory and bandwidth, potentially leading to performance issues.
1import { useEffect } from "react";
2
3export default function WebSocketComponent() {
4 useEffect(() => {
5 const socket = new WebSocket("wss://example.com/socket"); // Example WebSocket URL
6
7 // Do something with the WebSocket
8
9 return () => {
10 socket.close(); // Close the WebSocket connection
11 };
12 }, []);
13
14 return <div>. . .</div>;
In this example, when the component unmounts, socket.close()
will be executed to close the WebSocket connection.
You might not need effects
Effects are primarily used when a component needs to interact with or synchronize with external systems. Common use cases include fetching data from an API, subscribing to events, or updating the DOM outside of React’s normal rendering cycle, such as manipulating the browser's local storage or managing timers.
However, when no external systems are involved, it is a good time to revisit your code. Unnecessary effects can lead to excessive rerenders, reduced performance, and increased complexity in your code.
In such cases, you should try to simplify your logic by handling internal state updates directly within the component, instead of relying on side effects. Doing so will make your code more efficient and easier to maintain in the future.