What is useEffect() and How Does It Work? 🔍

React

readTime

14 min

What is useEffect() and How Does It Work? 🔍

The introduction of Hooks in React has transformed how we build components, and the useEffect hook is one of the most popular and frequently used ones—right alongside useState.

In essence, useEffect is the modern, simpler version of lifecycle methods from class-based components.

It helps us manage what happens when a component is mounted, updated, or unmounted, and does so in a much more understandable way.

In this post, I’ll break down everything you need to know about the useEffect hook, explaining its crucial role in React’s functional components and how it simplifies lifecycle management.


What is useEffect? 💡

The useEffect hook is a tool for handling side effects in functional components.

Side effects are operations that interact with the external world, such as fetching data, setting up subscriptions, or manipulating the DOM (Document Object Model).

useEffect allows you to perform these actions in a structured and efficient manner.


How to Use the useEffect Hook 🛠️

The basic syntax of useEffect is straightforward:

js
useEffect(() => {
  // Your side effect code here.
  // This code runs after every render of the component.

  return () => {
    // Optional cleanup code here.
    // This runs when the component is unmounted or before the effect is run again.
  };
}, [dependencies]); // Effect runs only when these dependencies change.
  • Effect Function: The function passed to useEffect runs after every render of the component. It's perfect for tasks like updating the DOM, fetching data, or setting up subscriptions.
  • Dependency Array: The second argument is a dependency array. useEffect only re-runs if these dependencies change between renders.

If you pass an empty array [], the effect runs once when the component is mounted, mimicking the behavior of componentDidMount in class components.

  • Cleanup Function: Optionally, the effect function can return a cleanup function. React calls this function before re-running the effect and when the component is unmounted. This is where you handle cleanup tasks, similar to componentWillUnmount in class components.

Transition from Class Components to Functional Components 🔁

In earlier versions of React, class components were the standard way to manage complex state and lifecycle events.

These components used specific lifecycle methods to execute code at different stages of a component’s lifecycle. Let’s look at a typical example:

Lifecycle Methods in Class Components

Class components in React use lifecycle methods to handle various stages of a component’s life. Here’s a common pattern using those methods:

js
class ExampleClassComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    document.title = `Clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `Clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    alert("Component is being removed");
  }

  render() {
    return (
      <div>
        <p>Clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

In this example, componentDidMount is used to run code after the component is inserted into the DOM, componentDidUpdate handles state or prop changes, and componentWillUnmount is used for cleanup before the component is removed.


Functional Components: The Introduction of Hooks

With the introduction of Hooks, lifecycle methods can now be handled in functional components, which are typically more concise, easier to read, and easier to test.

Here’s how the same functionality looks with useEffect in a functional component:

js
import React, { useState, useEffect } from "react";

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

  useEffect(() => {
    // Runs after every render (mounting and updating)
    document.title = `Clicked ${count} times`;

    // Cleanup code (runs when the component is unmounted)
    return () => {
      alert("Component is being removed");
    };
  }, [count]); // Effect runs again only if count changes

  return (
    <div>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

In a functional component, useEffect is used to handle side effects.

The first argument is a function that runs after every render, replacing both componentDidMount and componentDidUpdate because it can be configured to run only when specific values (like count) change.

The returned function inside useEffect serves as the cleanup mechanism, similar to componentWillUnmount in class components.


Practical Examples: Understanding useEffect in Action 🚀

Now, let’s dive into three different examples to demonstrate how the useEffect hook works as an equivalent to traditional lifecycle methods in class components.

These examples include data fetching (componentDidMount), reacting to state changes (componentDidUpdate), and cleanup operations (componentWillUnmount).

Example 1: Fetching Data on Mount (componentDidMount)

The useEffect hook can run code after the component is first rendered, just like componentDidMount. Let’s see this in action by fetching some data:

Fetching Data with useEffect

js
import React, { useState, useEffect } from "react";

function DataFetcher() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts/1")
      .then((response) => response.json())
      .then((post) => setData(post));
  }, []); // Empty array means it runs once on mount

  if (!data) {
    return <div>Loading...</div>;
  }

  return <div>Title: {data.title}</div>;
}

This example shows how useEffect can be used to fetch data when the component is mounted.

We passed an empty array of dependencies to ensure the effect only runs once, mimicking componentDidMount.


Example 2: Responding to State Changes (componentDidUpdate)

Next, let’s use useEffect to perform an action in response to state changes, similar to how componentDidUpdate works.

js
import React, { useState, useEffect } from "react";

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

  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]); // Effect runs only when count changes

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase</button>
    </div>
  );
}

This Counter component uses useEffect to update the document title every time the count state changes.

The dependency on [count] ensures the effect behaves like componentDidUpdate, running only when specific values change.


Example 3: Cleanup Operations (componentWillUnmount)

Finally, let’s see how useEffect can be used for cleanup tasks, similar to componentWillUnmount.

js
import React, { useState, useEffect } from "react";

function TimerComponent() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds((prevSeconds) => prevSeconds + 1);
    }, 1000);

    return () => clearInterval(intervalId); // Cleanup when the component is unmounted
  }, []); // Empty array means it runs once on mount

  return <div>Timer: {seconds} seconds</div>;
}

In this TimerComponent, we set up a timer that increments every second.

The cleanup function inside useEffect clears the interval when the component is unmounted, ensuring no unwanted side effects remain. This is similar to componentWillUnmount.


Understanding useEffect with Different Dependency Types 🧭

The behavior of the useEffect hook can vary significantly depending on the types of dependencies you provide. Let’s take a closer look at how it behaves with objects, null, undefined, and primitive types like strings and numbers.


1. No Dependency Array

If you omit the dependency array, useEffect runs after every render.

js
useEffect(() => {
  console.log("This runs after every render");
});

Without the dependency array, the effect runs after every render of the component. This is useful when you have an effect that needs to update every time the component re-renders, no matter what causes the update.


2. Empty Array []

An empty array means the effect runs once after the initial render, similar to componentDidMount.

js
useEffect(()

 => {
  console.log("This runs once after the initial render");
}, []);

With an empty array, the effect behaves like the componentDidMount lifecycle method in class components.


3. Array with Specific Values

When you include specific values in the array, the effect runs when those values change.

js
const [count, setCount] = useState(0);

useEffect(() => {
  console.log('This runs whenever "count" changes');
}, [count]);

Here, the effect runs every time the count state changes, allowing you to react to specific state or prop updates, similar to componentDidUpdate.


4. Using Objects {} as Dependencies

React doesn’t perform deep comparison in the dependency array. This means when you use objects as dependencies, React only compares their references, not their contents.

js
const [user, setUser] = useState({ name: "John", age: 30 });

useEffect(() => {
  console.log('Effect runs if the "user" object reference changes');
}, [user]);

In this snippet, useEffect re-runs only if the reference to the user object changes, not if its contents (like name or age) are updated.


5. null and undefined

Using null or undefined as dependencies behaves as if no dependency array was provided.

js
useEffect(() => {
  console.log("This runs after every render, like without a dependency array");
}, null); // or undefined

In this case, the effect runs after every render of the component, just like the default behavior when no dependency array is provided.


Common Mistakes and Best Practices When Using useEffect ⚠️💡

Using the useEffect hook can sometimes be tricky, so here are some common pitfalls and best practices to follow:


Common Mistakes

1. Infinite Render Loops

The Problem: Infinite loops occur when useEffect updates state or props that are also dependencies in the same array. This causes the effect to re-run endlessly.

Solution: Ensure you’re not updating state inside useEffect if that state is also in the dependency array.

js
useEffect(() => {
  const id = setInterval(() => {
    setCount((prevCount) => prevCount + 1);
  }, 1000);

  return () => clearInterval(id);
}, []); // Use an empty array to ensure it only runs once

2. Memory Leaks

The Problem: If useEffect contains asynchronous operations (e.g., fetching data) that are not properly cleaned up, memory leaks can occur.

Solution: Always return a cleanup function to ensure no leftover subscriptions or operations remain.

js
useEffect(() => {
  const controller = new AbortController();
  fetch("https://api.example.com/data", { signal: controller.signal })
    .then((response) => response.json())
    .then((data) => setData(data))
    .catch((error) => {
      if (error.name === "AbortError") return;
      console.error("Fetch error:", error);
    });

  return () => controller.abort();
}, []); // Ensure the effect runs only once

3. Incomplete Dependency Arrays

The Problem: Omitting variables in the dependency array can lead to unexpected behavior because useEffect won’t re-run when those variables change.

Solution: Make sure all variables used inside useEffect are included in the dependency array.

js
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(1);

useEffect(() => {
  console.log(`Count multiplied by multiplier: ${count * multiplier}`);
}, [count, multiplier]); // Ensure all relevant dependencies are listed

Best Practices

1. Use Appropriate Dependency Arrays

Clearly define which dependencies should trigger useEffect to re-run. An empty array ensures the effect runs only once after the initial render.

js
useEffect(() => {
  console.log("This runs once after the initial render");
}, []); // Effect runs only once

2. Split Effects into Multiple useEffect Hooks

Avoid cramming multiple different pieces of logic into a single useEffect. Instead, separate them into different useEffect hooks for better readability.

js
useEffect(() => {
  document.title = `Click count: ${count}`;
}, [count]); // Effect for updating the title

useEffect(() => {
  const intervalId = setInterval(() => {
    setSeconds((prevSeconds) => prevSeconds + 1);
  }, 1000);

  return () => clearInterval(intervalId);
}, []); // Effect for setting the timer

3. Use Cleanup Functions

Always return a cleanup function from useEffect to avoid memory leaks or unwanted side effects.

js
useEffect(() => {
  const subscription = someObservable.subscribe((data) => {
    setData(data);
  });

  return () => subscription.unsubscribe(); // Cleanup subscription
}, []); // Ensure the effect runs only once

4. Avoid Unnecessary Effects

Don’t include effects that don’t need to be updated with every state change. Consider carefully which effects actually require updates.

js
const [count, setCount] = useState(0);

useEffect(() => {
  document.title = `Click count: ${count}`;
}, [count]); // Effect runs only when `count` changes

Frequently Asked Questions About useEffect 🚀

1. What is the useEffect hook in React?

  • Answer: useEffect is a hook that allows you to manage side effects in functional components. Side effects include operations that interact with the external world, such as fetching data, setting up subscriptions, or manipulating the DOM. useEffect runs after the component renders and can return a cleanup function that runs before the effect is re-run or when the component is unmounted.

2. What are the differences between componentDidMount, componentDidUpdate, componentWillUnmount, and useEffect?

  • Answer: useEffect essentially combines the functionalities of componentDidMount, componentDidUpdate, and componentWillUnmount:
    • componentDidMount: useEffect with an empty dependency array [] runs once after the initial render, like componentDidMount.
    • componentDidUpdate: useEffect with specific dependencies runs after every render when those dependencies change, similar to componentDidUpdate.
    • componentWillUnmount: useEffect can return a cleanup function, which acts like componentWillUnmount, running before the component is removed or before the effect is re-run.

3. How does the dependency array in useEffect work?

  • Answer: The dependency array in useEffect specifies when the effect should re-run. If the array is empty [], the effect will only run once after the initial render. If the array contains specific values, the effect will re-run whenever those values change. If no array is provided, the effect runs after every render of the component.

4. Why can useEffect cause infinite render loops, and how can this be avoided?

  • Answer: Infinite loops can occur if useEffect updates state or props that are also dependencies in the same array. This causes the effect to re-run endlessly. To avoid this, make sure you’re not updating state inside useEffect if that state is also in the dependency array, and use cleanup functions to manage side effects.

5. How can you use useEffect to fetch data only once, after the component is first rendered?

  • Answer: To fetch data only once after the initial render, pass an empty dependency array [] to useEffect. This ensures the effect only runs once, just after the component is mounted.
js
useEffect(() => {
  fetch("https://api.example.com/data")
    .then((response) => response.json())
    .then((data) => setData(data));
}, []); // Empty dependency array

6. How do you implement cleanup operations using useEffect?

  • Answer: Cleanup operations can be implemented by returning a function from useEffect. This function will be called before the component is removed from the DOM or before the effect is re-run.
js
useEffect(() => {
  const subscription = someObservable.subscribe((data) => {
    setData(data);
  });

  return () => subscription.unsubscribe(); // Cleanup function
}, []); // Runs once after the initial render

7. What happens if you use objects {} as dependencies in the useEffect hook?

  • Answer: React performs shallow comparisons in the useEffect dependency array, meaning it only checks object references, not their contents. This means if you use objects as dependencies, React will only re-run the effect if the reference to the object changes, not if the object’s contents (like properties) are updated.

8. What are the best practices for using useEffect?

  • Answer: Best practices include:
    • Clearly defining dependencies in the array to ensure the effect re-runs at the right times.
    • Splitting effects into separate useEffect hooks to keep logic clean and isolated.
    • Returning cleanup functions to avoid memory

leaks.

  • Avoiding state updates inside useEffect if those states are also dependencies.

9. Can useEffect be used to listen for external context changes, such as window resize events?

  • Answer: Yes, useEffect can be used to listen for external context changes, such as window resize events. You can add an event listener in the useEffect and remove it in the cleanup function.
js
useEffect(() => {
  const handleResize = () => {
    console.log("Window resized");
  };

  window.addEventListener("resize", handleResize);
  return () => window.removeEventListener("resize", handleResize); // Cleanup function
}, []); // Empty dependency array, so effect runs only once

10. How can you avoid common pitfalls with useEffect?

  • Answer:
    • Always ensure all dependencies are correctly listed.
    • Use cleanup functions to prevent memory leaks.
    • Avoid updating state that’s also a dependency.
    • Test your effects to ensure they trigger as expected.

Following these guidelines helps avoid many useEffect-related issues and allows you to manage side effects efficiently in React components.


Conclusion ✅

In this post, we explored how the useEffect hook helps manage side effects in React components in a simple and efficient way.

It’s perfect for tasks like fetching data, updating the component, and performing cleanup operations.

Just remember to use it carefully to avoid common mistakes. Once you know how useEffect works and how to use it properly, you’ll be able to build some awesome projects.

Thanks for reading, and see you in the next post! 🎉

authorImg

Witek Pruchnicki

I passionately share knowledge about programming and more in various ways.