A Guide to React's useEffect Hook

A Guide to React's useEffect Hook

React's useEffect hook is one of the trickiest and most powerful hooks in functional components, allowing you to perform side effects. But what is a side effect? In React, side effect effectively means any operations that affect something outside 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 with examples.

When to use the effect hook

The useEffect hook should be used anytime you need to perform a side effect in your functional component. This can include 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 when you want to use the effect hook

  • Fetching the data from an API and updating the component's state based on the API's response.

  • Setting up a subscription to a data source and updating the component's state when new data is received.

  • Fetching/Persisting data from localStorage

  • Adding and removing event listeners.

Syntax of useEffect

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 re-run 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.

How to use the effect hook

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 to perform.

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,

Example 1: (without passing dependencies array)

When the dependency array is not specified, 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.

Example 2: (Passing an empty dependency array)

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 author's blog posts 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 a user's posts only once and rendering them to the DOM only once.

Some other scenarios where you will pass an empty dependency array.

  • When you want to update the page title when a particular page is visited.

  • When you want to send the analytics data to your backend when the user visits a particular page.

Example 3: (Passing arguments in 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.

Example 4: (With cleanup function)

We have not used the optional cleanup function in all the above examples. 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 are:

  • In a socket-based chat app, when the user leaves a chat room, we need to implement the cleanup function to disconnect from the socket.

  • If you use the useEffect hook to set up subscriptions to events or data, you should include a cleanup function that unsubscribes from those events or data when the component unmounts or the effect is re-run.

Session Replay for Developers

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data

OpenReplay

Happy debugging! Try using OpenReplay today.

How not to use it (With Example)

We saw various examples of using the useEffect hook in the previous section. In this section, we will see "How not to use it", i.e., the common mistakes developers make when using the useEffect hook.

Example 1:

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.

Example 2: (Not passing the empty dependency array)

If you don't include the empty dependency array when necessary, the useEffect will be re-run on every render, which could lead to performance issues in your application.

For example, consider the example 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, affecting the application's performance.

Example 3: (Adding unnecessary dependencies)

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 and should not be included in the dependencies array, and this could lead to the effect being re-run unnecessarily when the filter prop changes.

Example 4: (Not including the cleanup functions)

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 scenario we used in Example 4 of the previous section, but without 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 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 it is no longer rendered. This can lead to memory leaks, as the event listener will continue consuming resources even if they are 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.

Conclusion

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.

newsletter