The last useEffect Hook guide you’ll ever need

Tahera Alam
8 min read | Published on : Apr 19, 2023
Last Updated on : Dec 15, 2023





Table of Contents

Have you ever struggled to understand the useEffect hook in React? Look no further because this useEffect guide will be the last one you'll ever need! 

The useEffect hook is one of the most powerful features in React that allows you to perform side effects in functional components. If you don’t know what side effects are, don’t worry. We will pick it up in the following sections.

Understanding how to use the useEffect hook is crucial for any developer working with React applications. Whether you are a beginner or a pro, managing state and side effects can quickly become overwhelming without a solid grasp of useEffect. By mastering the useEffect hook, you can streamline your code, improve performance, and create better React applications.

In this useEffect hook tutorial, we'll  take a deep dive into how it works, from the basics of its syntax to advanced concepts like managing dependencies and cleanup functions. We’ll explore common use cases such as fetching data, updating the DOM, and cover best practices for working with useEffect. We'll also compare useEffect to lifecycle methods and other hooks like useLayoutEffect. 

By the end of this guide, you'll have a solid understanding of how to use the useEffect hook to improve the functionality and performance of your React applications. So, let's get started!

Basics of useEffect

Before we get to the more advanced concepts of the useEffect hook, let’s take a step back to understand the basics. It will help you have a solid foundation.

What useEffect does and how it works

In the beginning, we mentioned that the useEffect hook lets you perform side effects in functional components. If you don’t know what side effects are, in React, side effects (also called just “effects”) refer to operations such as fetching data from an API, modifying the DOM, or updating a component's state based on a prop change. 

The React useEffect Hook allows us to perform these side effects after a component has been rendered. It also allows us to perform cleanup operations, such as unsubscribing from an event or cancelling a request, before the component unmounts. 

If you have experience working with React class lifecycle methods, you can conceptualize the useEffect Hook as a combination of componentDidMount, componentDidUpdate, and componentWillUnmount.

Now that you know what the useEffect hook does, let’s see how it works. You don’t need to understand the syntax now. Just focus on how it works.

To use the useEffect hook, you simply need to pass in two arguments - a callback function and a dependency array. The callback function is where you put your side effects, and the useEffect dependency array tells useEffect when to run the function. If any of the values in the dependency array change, useEffect will re-run the callback function. The useEffect hook can also return a cleanup function that will run before the component unmounts or before the next time the effect runs.

Let’s look at an example of using useEffect to better understand this:

    
export default function App() { const [count, setCount] = useState(0); useEffect(() => { document.title = `Count: ${count}`; }, [count]); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }

In this example, we declare the count state variable, and then we tell React we need to use an effect. We pass a function to the useEffect Hook. This function that we pass is our effect. Inside our effect, we set the document title using the document.title browser API. The React useEffect hook runs after the component is rendered and whenever the count variable changes, the effect function updates the document title to the current count.

Notice how we passed the count variable in the dependency array to tell React that every time the count changes, we want our effect to be re-run.

In this effect, we set the document title, but we could also perform data fetching or event subscription etc.

The syntax of useEffect

The useEffect hook has a fairly simple syntax. It takes two parameters: a callback function that represents the side effects and an optional dependency array.

    
useEffect( () => { // callback function that performs side effects }, [dependency]);

The first parameter is the callback function that contains the code for the side effect you want to perform. This function will run every time your component renders. The second parameter is an optional dependency array. This parameter is used to tell React when the effect should be re-run. If we pass an empty array, the effect will only run once, i.e after the initial render. But if we pass a non-empty array, if any of the values in this array change, the effect will be re-run.

Managing dependencies with useEffect

Managing dependencies effectively is crucial while using useEffect. As we saw in the previous section, the dependency array is the second argument that we pass to useEffect and it determines when the effect should be re-run. If any of the dependencies change, useEffect will re-run the code inside it. But if none of the dependencies change, useEffect won't re-run, preventing unnecessary re-renders.

But there’s a catch: if you don't include all the necessary dependencies in the array, your effect could behave unexpectedly. For example, suppose you have a profile component and you are making an API call in the useEffect to get the user’s name when the component renders. If you don’t include that name variable in the dependency array, your component might not update when that name changes which we don’t want. 

To understand when to use dependency and when not to, let’s look at a few examples.

1. Fetching data with useEffect

Let's say we have a component that fetches data from an API when it is mounted. We want to ensure that the effect only runs once, so we pass an empty dependency array.

 useEffect(() => {
    // This effect will run only once, when the component is mounted
   const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    setData(data);
  }, []);// pass empty dependency array to ensure the effect runs only once

In this example, we pass an empty dependency array to the useEffect hook to ensure that the effect only runs once, when the component mounts. If we didn't pass an empty dependency array, the effect would run on every re-render, causing unnecessary network requests.

2. Updating a component state based on props

This is a very common use case of useEffect. Suppose we have a component that receives a prop from its parent component, and we want to update our local state when the prop changes. In this case, we pass the prop as a dependency to the useEffect hook. 

function UserProfile(props) {
    const [name, setName] = useState("");

    useEffect(() => {
      setName(props.name);
    }, [props.name]); // pass props.name as a dependency to update the local state when the prop changes

    return <h1>Hello, {name}</h1>;
  }

In this example, we pass the props.name as a dependency to useEffect to ensure that the effect runs whenever the name prop changes. If we didn't pass the name prop as a dependency, the effect would not update the local state when the prop changes, causing the component to display the old name, in this case empty string.

useEffect cleanup function

There are two common kinds of side effects in React components: those that don’t require cleanup, and those that do. 

In the previous section, we saw the counter-example that did not need a cleanup. Side effects that don't require cleanup are those that don't involve modifying any external state or resources. These might include network requests, logging, or updating the component's state.

But some effects do require a cleanup. Examples include setting up event listeners or creating timers etc. If you don't clean up after these side effects, they can cause memory leaks, slow down your app, and interfere with other parts of your code. 

To clean up after side effects, you can return a function from your useEffect hook that will be called when the component is unmounted or the dependencies of the useEffect hook change. This function should contain the code to undo any side effects created by the useEffect hook.

Let’s look at an example:

function ExampleComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const handleKeyPress = (event) => {
      if (event.keyCode === 32) {
        setCount(count + 1);
      }
    };
    window.addEventListener("keydown", handleKeyPress);

    // Clean up the event listener
    return () => {
      window.removeEventListener("keydown", handleKeyPress);
    };
  }, [count]);

  return (
    <div>
      <p>Press the space bar to increase count: {count}</p>
    </div>
  );
}

export default ExampleComponent;

In this example, the useEffect hook adds an event listener to the window object that listens for the space bar key press and updates the count state variable. The cleanup function removes the event listener when the component is unmounted or when the count state variable changes. 

If we didn't use the cleanup function to remove the event listener, it would continue to exist even after the component is unmounted. This can cause the event listener to still be active and use memory resources even though the component is no longer in use, leading to a memory leak. It can also cause unexpected behavior in our application as the event listener is still active and updating state variables even though the component is no longer in the DOM.

By using the cleanup function in useEffect, we ensured that the event listener is only active when the component is mounted.

Order of Execution of Effects in useEffect Hook

When using the useEffect hook in React application, it's important to understand the order of execution of effects. The order in which the effects are executed can have a significant impact on the behavior of your component.

React determines the order of execution of effects based on two factors: the order in which they are defined and their dependencies. The order in which the effects are defined is important because React will execute them in the order they appear in the code. For example, if you have two effects, effectA, and effectB, and if effectB depends on the result of effectA, then you should define effectA before effectB.

In addition to the order of definition, React also considers the dependencies of the effects. React will execute the effects with no dependencies first, followed by effects with dependencies. When an effect has dependencies, React will only re-execute it if one of the dependencies has changed since the last render.

It's also important to note that the order of execution can be affected by other factors as well, such as whether the component is mounted or unmounted, or whether the effect is a cleanup function.

By following some best practices which we will discuss in one of the later sections and being mindful of the order and dependencies of effects, you can avoid unexpected behavior in your component and ensure that it functions as intended.

Rules of Hooks

Before we continue with the useEffect hook, let's discuss some fundamental rules for using hooks. Although they are not restricted to the useEffect Hook, it's crucial to know where in your code effects can be defined. 

To use hooks, you must adhere to certain rules:

  • Only the top-level function that makes up your functional React component can call hooks.
  • Nesting code may not be used to invoke hooks (e.g. conditions, loops, or another function body)
  • Custom Hooks are special functions that can call Hooks from within them, including the top-level function. Moreover, rule two also applies to them.

When not to use useEffect

While the React’s useEffect hook is a powerful tool for managing side effects in React applications, it's not always the best tool for the job. In fact, using useEffect in certain situations can actually make your code more complex and difficult to maintain.

So, when should you avoid using useEffect? Here are a few cases to consider:

Transform data for rendering

When it comes to transforming data before rendering in React, you might be tempted to use the useEffect hook. For example, if you want to show a list of active users, you might create a state variable and update it with an effect whenever the user list changes. However, this is unnecessary and inefficient. 

Instead of this:

function UserList({ users }) {
    const [activeUsers, setActiveUsers] = useState([]);

    useEffect(() => {
      // filter the user list and update the state whenever the users prop changes
      setActiveUsers(users.filter((user) => user.isActive));
    }, [users]);

    return (
      <div>
        <h1>Active Users</h1>
        <ul>
          {activeUsers.map((user) => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      </div>
    );
  }

Instead, you can simply filter the user list directly and display the filtered results without using useEffect. This not only saves you time but also improves the performance of your application. 

Do this:

function UserList({ users }) {
    const [activeUsers, setActiveUsers] = useState([]);

    // filter the user list directly and update the state
    setActiveUsers(users.filter((user) => user.isActive));

    return (
      <div>
        <h1>Active Users</h1>
        <ul>
          {activeUsers.map((user) => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      </div>
    );
  }

Handling user events

Instead of using useEffect for handling user events, use event handlers directly in your React components. This simplifies your code and reduces the risk of future issues.

To make a POST request when a user submits a form, you can define the handleSubmit function to create the JSON payload and make the request directly inside the function. Then, attach the handleSubmit function to the form's onSubmit event to trigger the POST request when the form is submitted. Here’s an example:

function Form() {
    function handleSubmit(e) {
      e.preventDefault();
      const jsonToSubmit = { firstName, lastName };
      post("/api/register", jsonToSubmit);
    }
  return (
     <form onSubmit={handleSubmit}>
        <input type="text" name="firstName" />
        <input type="text" name="lastName" />
        <button type="submit">Submit</button>
      </form>
    );
  }

This approach makes your code cleaner, more concise, and easier to debug. Plus, it eliminates the need for an additional state variable and avoids the potential for cascading effects.

Using React’s useEffect hook with lifecycle methods 

In one of the earlier sections, we mentioned that useEffect hook is componentDidMount, componentDidUpdate, and componentWillUnmount combined. Let’s understand how it replaces these lifecycle methods.

useEffect(
() => {
   // perform side effects here
 }, [dependencies]);

As we discussed previously, useEffect hook tells that our component needs to update after a render. It runs after the first render and after every update providing the fact that one of the dependencies has changed since the last render.

Let’s now look at an example to understand how to replicate the functionality of componentDidMount, componentDidUpdate and componentWillUnmount with a single useEffect hook.

  • componentDidMount: This method is called when the component is first mounted(inserted) in the DOM. Side effects like fetching data, setting up event listeners or initializing third party libraries are performed here. You can use useEffect to perform these same operations.
useEffect(() => {
    // Perform side effects here
  }, []);

  • componentDidUpdate: This method is called when the component is updated. You can use useEffect hook’s dependency array to perform any side effects that need to happen when the component updates, such as fetching new data based on props or state changes.          
useEffect(() => {
    // Perform side effects here
  }, [props, state]);

  • componentWillUnmount: This lifecycle method is called when the component is unmounted or removed from the DOM. Clean up tasks such as removing event listeners or clearing timeouts are performed here. You can use useEffect hook’s return function to perform any cleanup that needs to happen.
useEffect(() => {
   return () => {
     // Perform cleanup here
   };
 }, []);

These are a few examples of how useEffect replicates the traditional lifecycle methods. By using useEffect, you can write more concise and readable code which is an essential aspect of any application. You can go through our guide on React’s useEffect Hook with lifecycle methods and dive deeper in this topic.

useEffect vs componentDidMount

Both useEffect and componentDidMount serve the same purpose of executing side effects after a component has been mounted. But there are some key differences between the two that you should be aware of. 

componentDidMount is a lifecycle method that is called after the component has been mounted, and it only runs once in the lifecycle of the component. It is often used to perform actions that require the component to be mounted first, such as fetching data from an API or initializing third-party libraries.

Let’s take a look at an example of using componentDidMount to fetch some data:

class MyComponent extends React.Component {
  componentDidMount() {
    fetch("/api/data")
      .then((response) => response.json())
      .then((data) => this.setState({ data }));
  }
  render() {
    return <div>{this.state.data}</div>;
  }
}

Now, let’s look at an example of doing the same using the useEffect hook:

function MyComponent() {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetch("/api/data")
      .then((response) => response.json())
      .then((data) => setData(data));
  }, []);

  return <div>{data}</div>;
}

Unlike the componentDidMount, useEffect hook can be called multiple times during the lifecycle of the component and it is often used to replace componentDidMount, componentDidUpdate, and componentWillUnmount altogether.

So, when should you use useEffect instead of componentDidMount? The answer is simple: whenever you can! useEffect is more flexible and easier to use than componentDidMount, and it is the recommended way to perform side effects in functional components. 

useEffect vs useLayoutEffect

React provides two hooks, useEffect and useLayoutEffect, that allow us to perform side effects in functional components. While both hooks are similar, they have some important differences that are worth understanding.

The main difference between the two hooks is the timing of when they are executed. The useEffect is executed after the browser has painted the screen, while useLayoutEffect is executed before the browser paints the screen. This means that useLayoutEffect is more suitable for performing synchronous layout-related tasks.

For example, if you have an effect that changes the size or position of an element, you should use useLayoutEffect to ensure that the layout is updated synchronously. On the other hand, if you have an effect that fetches data from an API, you should use useEffect because it doesn't need to be synchronous.

Let’s look at an example to demonstrate the difference between the two:

export default function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("useEffect", count);
  }, [count]);

  useLayoutEffect(() => {
    console.log("useLayoutEffect", count);
  }, [count]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Count: {count}</p>
      <div
        style={{
          width: `${count * 50}px`,
          height: `${count * 50}px`,
          background: "red"
        }}
      ></div>
    </div>
  );
}

In this example, we're rendering a red box that changes size as count increases. We're also logging the current value of count using both useEffect and useLayoutEffect.

If you run this code and click the increment button, you'll see that layout updates are slightly delayed when using useEffect. This is because useEffect is executed after the browser has painted the screen. On the other hand, useLayoutEffect is executed before the browser paints the screen, so the layout changes are visible immediately.

However, keep in mind that useLayoutEffect can cause performance issues if the effect is too slow, so you should use it sparsely. If you are interested in an in-depth explanation of when to use each hook, I would recommend you go through the useEffect vs useLayoutEffect guide.

Best practices for working with useEffect 

It's important to use useEffect correctly to ensure that your components behave as expected and avoid performance issues. So, let’s look at some best practices for working with useEffect that can help you use it effectively.

  1. Use multiple useEffect hooks: If you have multiple effects that should run under different conditions, it’s often better to use multiple useEffect hooks instead of combining everything into one. This allows you to keep your code organized and more readable.
  2. Always define the dependencies array: One of the most important best practices for working with useEffect is to always define the dependencies array. This array tells React when the effect should run, so it's important to get it right.
  3. Be mindful of the order of execution: The order of execution of effects is important, so make sure that you understand how React determines the order and how it affects your component's behavior.
  4. Use cleanup functions: To avoid memory leaks and unexpected behavior, use cleanup functions to clean up any side effects that your component may have created, such as event listeners or timers. 
  5. Avoid triggering unnecessary re-renders: Be mindful of the dependencies array and make sure that your effect only runs when it needs to. Avoid triggering unnecessary re-renders by only updating the state or props that are actually needed.
  6. Avoid using useEffect for all side effects: While useEffect is a powerful tool, it's not always the best option for every situation. Avoid using it for handling user events or transforming data for rendering, and consider using other techniques like React's state management or event handling system.

Wrapping it up!

The useEffect hook is a crucial tool in a React developer's arsenal. It enables us to manage side effects, fetch data, and update state without cluttering our components.

In this guide,we have covered the basics of useEffect, managing dependencies, cleanup functions, and useEffect’s order of execution. Additionally, we explored how to use useEffect to replace lifecycle methods and went through the differences between useEffect with useLayoutEffect. While it's essential to follow the rules of hooks, it's also important to know when not to use useEffect and explore other techniques. By mastering this essential tool, you can create better React applications and take your skills to the next level.

I hope this guide has been helpful in providing you with the knowledge and confidence to use the useEffect hook effectively. So, go forth and create amazing React applications! Until next time, happy coding!

Wanna try Zipy?

Zipy provides you with full customer visibility without multiple back and forths between Customers, Customer Support and your Engineering teams.

The unified digital experience platform to drive growth with Product Analytics, Error Tracking, and Session Replay in one.

product hunt logo
G2 logoGDPR certificationSOC 2 Type 2
Zipy is GDPR and SOC2 Type II Compliant
© 2024 Zipy Inc. | All rights reserved
with
by folks just like you