The useEffect Hook

The useEffect Hook is one of the most important and frequently used Hooks in React. It is used to perform side effects in functional components — actions that interact with something outside of React's rendering process.

Side effects include things like:

  • Fetching data from an API
  • Setting up timers or intervals
  • Updating the page title
  • Adding or removing event listeners
  • Interacting with browser storage

These operations cannot be placed directly inside the component function body, because React may call that function multiple times during rendering. useEffect provides a safe place to run these operations after rendering is complete.

Importing useEffect


import { useState, useEffect } from 'react';

Basic Syntax of useEffect


useEffect(() => {
  // Side effect code goes here
}, [dependencies]);

useEffect takes two arguments:

  1. A callback function — the code to run as the side effect
  2. A dependency array — controls when the effect runs

The Dependency Array — When Does useEffect Run?

The second argument (the dependency array) is critical. It determines when the effect is triggered:

No Dependency Array — Runs After Every Render


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

Omitting the dependency array means the effect runs every time the component re-renders. This is rarely what is needed and can cause performance issues.

Empty Array [] — Runs Only Once (On Mount)


useEffect(() => {
  console.log("This runs only once when the component first appears");
}, []);

An empty dependency array means the effect runs once when the component is first added to the page (mounted). This is the most common use case for data fetching.

With Dependencies — Runs When Dependencies Change


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

When specific values are placed in the array, the effect re-runs only when those values change. This is useful for reacting to prop or state changes.

Example — Updating the Page Title


import { useState, useEffect } from 'react';

function PageTitleUpdater() {
  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>
  );
}

Every time count changes, the effect runs and updates the browser tab title. This is a simple but common real-world use of useEffect.

Example — Fetching Data from an API

Data fetching is one of the most common uses of useEffect. The effect runs once after the component mounts, fetches data, and stores it in state:


import { useState, useEffect } from 'react';

function UserList() {
  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);
      });
  }, []); // Empty array = runs once on mount

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

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

The empty dependency array ensures the fetch only happens once. The loading state shows feedback to the user while the data is being retrieved.

Cleanup Functions

Some effects need to be cleaned up when a component is removed from the page (unmounted). For example, timers and event listeners should be stopped or removed to prevent memory leaks.

A cleanup function is returned from inside the useEffect callback:


import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

    // Cleanup function — clears the interval when component unmounts
    return () => {
      clearInterval(interval);
    };
  }, []); // Runs once on mount

  return <p>Time elapsed: {seconds} seconds</p>;
}

The function returned inside useEffect is the cleanup function. React calls it automatically when the component is removed from the screen, preventing the interval from continuing to run in the background.

Running an Effect Based on a Prop


import { useEffect } from 'react';

function ProfileView({ userId }) {
  useEffect(() => {
    console.log(`Fetching profile for user ID: ${userId}`);
    // Imagine an API call here
  }, [userId]); // Re-runs every time userId changes

  return <p>Viewing profile for user {userId}</p>;
}

When the parent component changes the userId prop, the effect re-runs and fetches new profile data. The dependency array is the key to controlling this behavior.

Common Mistakes with useEffect

  • Missing dependencies: If a variable used inside the effect is not included in the dependency array, the effect may use a stale value.
  • Infinite loops: If state is updated inside an effect without proper dependencies, it can create an infinite loop (state changes → effect runs → state changes again).
  • Not cleaning up: Forgetting to return a cleanup function from effects with timers or subscriptions causes memory leaks.

Key Points

  • useEffect is used to run side effects like data fetching, timers, and DOM manipulation.
  • The dependency array controls when the effect runs: no array (every render), [] (once on mount), or [value] (when value changes).
  • Always include variables used inside the effect in the dependency array.
  • Return a cleanup function from the effect to handle resource teardown when the component unmounts.
  • Avoid creating infinite loops by being careful about which values trigger re-runs.

The next topic introduces the useRef Hook — a way to access DOM elements and persist values across renders without triggering re-renders.

Leave a Comment

Your email address will not be published. Required fields are marked *