DEV Community

Keyur Gohil
Keyur Gohil

Posted on

Understanding the useEffect Hook in React: A Practical Guide with Examples

React is all about building interactive user interfaces. But sometimes, our components need to interact with the outside world.

For example:

  • Fetching data from an API
  • Updating the browser tab title
  • Setting up timers
  • Adding event listeners
  • Working with WebSockets
  • Subscribing to external services

These operations are called side effects.

In React, the useEffect Hook is used to handle these side effects.

What is useEffect?

useEffect is a built-in React Hook that allows you to perform side effects in functional components.

import { useEffect } from "react";

useEffect(() => {
  // Side effect code
}, []);
Enter fullscreen mode Exit fullscreen mode

The useEffect Hook runs after the component renders.

A basic example:

import { useEffect } from "react";

function App() {
  useEffect(() => {
    console.log("Component rendered");
  }, []);

  return <h1>Hello React</h1>;
}

export default App;
Enter fullscreen mode Exit fullscreen mode

In this example, the effect runs after the component is mounted.

What is a Side Effect?

A side effect is an operation that happens outside the normal React rendering process.

Common examples include:

  • API calls
  • Browser document updates
  • Timers
  • Event listeners
  • WebSocket connections
  • Local storage operations

For example, updating the document title is a side effect:

import { useEffect } from "react";

function App() {
  useEffect(() => {
    document.title = "My React App";
  }, []);

  return <h1>Welcome to React</h1>;
}

export default App;
Enter fullscreen mode Exit fullscreen mode

After the component renders, the browser tab title becomes My React App.

Understanding the Dependency Array

The second argument of useEffect is called the dependency array.

It controls when the effect should run.

1. Without a Dependency Array

useEffect(() => {
  console.log("Effect executed");
});
Enter fullscreen mode Exit fullscreen mode

This effect runs after every render.

import { useEffect, useState } from "react";

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

  useEffect(() => {
    console.log("Component rendered");
  });

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

export default Counter;
Enter fullscreen mode Exit fullscreen mode

Every time the state changes, the component re-renders and the effect runs again.


2. Empty Dependency Array

useEffect(() => {
  console.log("Component mounted");
}, []);
Enter fullscreen mode Exit fullscreen mode

An empty dependency array means the effect runs only after the component is mounted.

This is commonly used for fetching initial data.

useEffect(() => {
  fetchData();
}, []);
Enter fullscreen mode Exit fullscreen mode

3. With Dependencies

useEffect(() => {
  console.log("Count changed");
}, [count]);
Enter fullscreen mode Exit fullscreen mode

This effect runs whenever the count value changes.

Example: Updating the Document Title

Let's create a counter application:

import { useEffect, useState } from "react";

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

  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  return (
    <div>
      <h1>Count: {count}</h1>

      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

export default Counter;
Enter fullscreen mode Exit fullscreen mode

Whenever count changes, the browser tab title also changes.

The dependency array is:

[count]
Enter fullscreen mode Exit fullscreen mode

This tells React:

Run this effect whenever count changes.

Fetching Data Using useEffect

One of the most common use cases of useEffect is fetching data from an API.

import { useEffect, useState } from "react";

function Users() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users")
      .then((response) => response.json())
      .then((data) => {
        setUsers(data);
        setLoading(false);
      });
  }, []);

  if (loading) {
    return <p>Loading...</p>;
  }

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default Users;
Enter fullscreen mode Exit fullscreen mode

How does this work?

When the component mounts:

useEffect(() => {
  // API call
}, []);
Enter fullscreen mode Exit fullscreen mode

The API request runs.

After the data is received:

setUsers(data);
Enter fullscreen mode Exit fullscreen mode

React updates the state and re-renders the component.

Fetching Data When a Value Changes

Suppose we want to fetch posts for a specific user.

import { useEffect, useState } from "react";

function UserPosts({ userId }) {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch(
      `https://jsonplaceholder.typicode.com/posts?userId=${userId}`
    )
      .then((response) => response.json())
      .then((data) => setPosts(data));
  }, [userId]);

  return (
    <div>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </article>
      ))}
    </div>
  );
}

export default UserPosts;
Enter fullscreen mode Exit fullscreen mode

Whenever userId changes, the effect runs again and fetches the new user's posts.

Cleanup Function in useEffect

Some effects need to be cleaned up when a component is removed from the screen.

Examples include:

  • Timers
  • Event listeners
  • WebSocket connections
  • Subscriptions

The cleanup function is returned from the effect.

useEffect(() => {
  // Setup

  return () => {
    // Cleanup
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

Example: Timer with Cleanup

import { useEffect, useState } from "react";

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

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

    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return <h1>{seconds} seconds</h1>;
}

export default Timer;
Enter fullscreen mode Exit fullscreen mode

Here, the timer starts when the component mounts.

When the component is unmounted, the cleanup function runs:

clearInterval(intervalId);
Enter fullscreen mode Exit fullscreen mode

This prevents memory leaks.

Example: Event Listener

import { useEffect, useState } from "react";

function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }

    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return <h1>Window Width: {width}px</h1>;
}

export default WindowWidth;
Enter fullscreen mode Exit fullscreen mode

The event listener is added when the component mounts.

When the component is removed, the listener is removed as well.

Avoid Infinite Loops

Be careful when updating a state variable inside an effect that also depends on that same state.

❌ Bad example:

import { useEffect, useState } from "react";

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

  useEffect(() => {
    setCount(count + 1);
  }, [count]);

  return <h1>{count}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

This creates an infinite loop:

  1. count changes.
  2. The effect runs.
  3. setCount changes count.
  4. The component renders again.
  5. The effect runs again.

And the cycle continues.

Don't Use useEffect for Everything

A common mistake is using useEffect when it is not necessary.

For example:

const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Enter fullscreen mode Exit fullscreen mode

This is unnecessary.

Instead, simply calculate the value:

const fullName = `${firstName} ${lastName}`;
Enter fullscreen mode Exit fullscreen mode

This is simpler and avoids an unnecessary state update.

useEffect vs Event Handlers

Use an event handler when something happens because of a user action.

For example:

function Button() {
  function handleClick() {
    console.log("Button clicked");
  }

  return (
    <button onClick={handleClick}>
      Click Me
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

You do not need useEffect for this.

Use useEffect when you need to synchronize your component with an external system.

Simple Mental Model

You can remember the React flow like this:

State or Props Change
        ↓
Component Renders
        ↓
React Updates the DOM
        ↓
useEffect Runs
Enter fullscreen mode Exit fullscreen mode

This makes useEffect useful for tasks that should happen after rendering.

When Should You Use useEffect?

Use useEffect when you need to:

  • Fetch data from an API
  • Update the document title
  • Set up timers
  • Add event listeners
  • Subscribe to external services
  • Connect to WebSockets
  • Work with browser APIs
  • Synchronize with third-party libraries

When Should You Avoid useEffect?

You probably do not need useEffect when:

  • You are calculating a value from existing state or props.
  • You are responding directly to a user action.
  • You can perform the operation during rendering.
  • You are using an effect only to update another state unnecessarily.

Conclusion

The useEffect Hook is one of the most important Hooks in React.

The key points to remember are:

  1. useEffect is used to handle side effects.
  2. The dependency array controls when the effect runs.
  3. An empty dependency array runs the effect after the initial mount.
  4. Dependencies should include values used inside the effect.
  5. Cleanup functions are important for timers, event listeners, and subscriptions.
  6. Avoid using useEffect when a simple calculation or event handler is enough.

Once you understand when an effect should run and why it should run, using useEffect becomes much easier.

Happy Coding! 🚀

Top comments (0)