How to Use useEffect Hook in React

This article provides a straightforward guide on how to implement the useEffect hook in React. You will learn its core syntax, how to control its execution using the dependency array, and how to write cleanup functions to prevent memory leaks. By the end of this guide, you will be able to confidently handle side effects like data fetching, subscriptions, and manual DOM updates in your React components.

What is the useEffect Hook?

In React, the useEffect hook allows you to perform side effects in functional components. Side effects are actions that happen outside the scope of the normal component rendering process, such as fetching data from an API, setting up event listeners, or interacting directly with the browser DOM.

Basic Syntax

To use useEffect, you must import it from the React library. The hook accepts two arguments: a callback function containing the side effect logic, and an optional dependency array.

import { useEffect } from 'react';

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

Controlling when useEffect Runs

The behavior of useEffect depends entirely on what you pass into the second argument (the dependency array). There are three common scenarios:

1. Running on Every Render (No Dependency Array)

If you omit the dependency array entirely, the effect will run after the initial render and after every subsequent update of the component.

useEffect(() => {
  console.log('This runs on every single render');
});

2. Running Only Once on Mount (Empty Dependency Array)

If you pass an empty array ([]), the effect runs exactly once when the component is first mounted to the screen. This is ideal for initial data fetching or setting up global event listeners.

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

3. Running When Specific Values Change (With Dependencies)

If you pass variables (props or state) into the dependency array, the effect will run on the initial mount and then rerun only when those specific variables change between renders.

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

useEffect(() => {
  console.log(`The count is now ${count}`);
}, [count]); // Only reruns when 'count' changes

Adding a Cleanup Function

Some side effects require cleanup to avoid memory leaks, such as clearing intervals, closing WebSocket connections, or removing event listeners. To clean up an effect, return a function from your useEffect callback. React will execute this cleanup function before the component unmounts and before running the effect again.

useEffect(() => {
  const handleResize = () => {
    console.log(window.innerWidth);
  };

  window.addEventListener('resize', handleResize);

  // Cleanup function
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

Example: Fetching Data with useEffect

Here is a practical, real-world example of using useEffect to fetch data from an API when a component mounts:

import React, { 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);
      })
      .catch((error) => {
        console.error('Error fetching data:', error);
        setLoading(false);
      });
  }, []); // Empty array ensures this only runs once

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

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

export default UserList;