A Detailed Guide to the useEffect Hook in React

Written by nirmalkumar | Published 2023/02/13
Tech Story Tags: react | webdev | javascript | react-hooks | useeffect | web-development | reactjs | javascript-top-story

TLDRReact's useEffect hook is one of the trickiest and most powerful hooks in functional components. It allows you to perform side effects 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 should be used anytime you need your functional component to perform a side effect.via the TL;DR App

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.

When to use the effect hook

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:

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

useEffect syntax

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.

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 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,

Example 1: Without passing dependency array

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.

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 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 you want to update the title of the page when a particular page is visited.
  • When you want to send the analytics data to your backend when the user visits a particular page. (For eg., Page visit counter)

Example 3: Passing arguments in the 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 the cleanup function

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

How not to use the effect hook (with examples)

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.

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

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

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

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.


Also published here.


Written by nirmalkumar | Software Developer with a newly found passion for writing
Published by HackerNoon on 2023/02/13