React useEffect

useEffect lets a function component run side effects — data fetching, subscriptions, timers, manual DOM work — after React commits changes to the screen.

Running Code After Render

useEffect takes a callback function and runs it after React has painted the component to the screen, not during rendering itself. This keeps rendering pure and predictable while giving you a safe place to reach outside React — to the network, the DOM, timers, or browser APIs.

The Dependency Array

The second argument to useEffect, the dependency array, controls how often the effect re-runs. Omit it entirely and the effect runs after every single render. Pass an empty array and it runs exactly once, right after mount. Pass an array of values and it re-runs after mount and again whenever any of those values changes between renders.

  • Fetching data from an API when a component mounts or a query parameter changes
  • Subscribing to an external event source (WebSocket, DOM event, third-party library)
  • Setting up timers or intervals
  • Syncing with browser APIs, like setting document.title
  • Logging or sending analytics events

Fetching data on mount

import { useState, useEffect } from "react";

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

  useEffect(() => {
    let cancelled = false;

    async function loadUsers() {
      const response = await fetch("/api/users");
      const data = await response.json();
      if (!cancelled) {
        setUsers(data);
        setLoading(false);
      }
    }

    loadUsers();

    return () => {
      cancelled = true;
    };
  }, []);

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

export default UserList;

An effect can return a cleanup function. React calls it right before the effect runs again (to tear down the previous run) and one final time when the component unmounts. This is how you cancel subscriptions, clear timers, or ignore results that arrive after a component no longer cares about them.

Interval with cleanup

import { useState, useEffect } from "react";

function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const [running, setRunning] = useState(true);

  useEffect(() => {
    if (!running) return;

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

    return () => clearInterval(id);
  }, [running]);

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={() => setRunning((prev) => !prev)}>
        {running ? "Pause" : "Resume"}
      </button>
    </div>
  );
}

export default Stopwatch;
Note: If an effect sets state unconditionally on every run and has no dependency array, it re-runs after that state update triggers a render, which triggers the effect again — an infinite loop. Always give effects an accurate dependency array.

Subscribing to a browser event

import { useState, useEffect } 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 <p>Window width: {width}px</p>;
}

export default WindowWidth;
Dependency arrayRuns
(omitted)After every render, including every re-render caused by any state or prop change
[]Once, right after the component mounts
[a, b]After mount, and again any time a or b changes between renders
Note: Omitting a variable the effect actually reads from its dependency array creates a stale closure that keeps using the value from the render the effect was created in; install eslint-plugin-react-hooks to catch this automatically.

Exercise: React useEffect

With an empty dependency array [], when does the effect run?