How to Create Side Effects in React

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.

jsx
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).

jsx
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.

jsx
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.

jsx
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.

jsx
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.

jsx
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:

jsx
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.

Display articles for user

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:

jsx
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

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

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.

text
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.

jsx
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.