How to Use useEffect in React

What you’ll build or solve

You’ll run side effects in a React component at the right time, like syncing with browser APIs, timers, or event listeners.

When this approach works best

useEffect works best when you need to:

  • Sync with something outside React, like document.title, localStorage, or a third-party widget.
  • Set up and clean up timers, event listeners, or subscriptions.
  • React to a specific prop or state change with a side effect, like updating the URL or focusing an input.

Skip useEffect when you can compute the value during render. If the result depends only on props and state, derive it directly instead of storing it via an effect.

Prerequisites

  • A React app set up (Vite, CRA, Next.js, etc.)
  • Basic React knowledge (components, props, state)

Step-by-step instructions

1) Add an effect that runs after render

useEffect runs after React finishes rendering and updates the DOM. Put your side-effect code in the callback.

import { useEffect, useState } from "react";

export default function Profile() {
  const [name, setName] = useState("Sam");

  useEffect(() => {
    document.title = `Profile: ${name}`;
  });

  return (
    <div>
      <p>{name}</p>
      <button onClick={() => setName("Alex")}>Change name</button>
    </div>
  );
}

Without a dependency array, the effect runs after every render. That works for cheap sync work, but it can be too much for effects that set up ongoing work.


2) Control when effects run with the dependency array

The dependency array (the second argument) changes when the effect runs.

Run once on mount

Use [] to run the effect when the component mounts.

import { useEffect } from "react";

export default function Welcome() {
  useEffect(() => {
    console.log("Mounted");
  }, []);

  return <p>Welcome</p>;
}

Run when values change

List the values your effect uses. React reruns the effect when any dependency changes.

import { useEffect, useState } from "react";

export default function CounterTitle() {
  const [count, setCount] = useState(0);

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

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

Run after every render (rare)

Omit the array when you truly want the effect after every render.

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

Use this sparingly. If you see repeated work you did not expect, a missing dependency array is often the reason.

What to look for:

  • Include every prop and state value you read inside the effect in the dependency array.
  • If a dependency changes every render (like an inline object or function), the effect will also rerun every render.

3) Clean up side effects with a return function

If your effect sets up something that keeps running, return a cleanup function. React runs cleanup before rerunning the effect and when the component unmounts.

import { useEffect, useState } from "react";

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

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

    window.addEventListener("resize", onResize);

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

  return <p>Width: {width}px</p>;
}

What to look for:

  • Cleanup runs before the next effect run, not just on unmount.
  • Timers, listeners, subscriptions, and in-flight async work should not keep running after unmount.

Examples you can copy

Example 1: Sync document.title with state

import { useEffect, useState } from "react";

export default function TitleSync() {
  const [name, setName] = useState("Sam");

  useEffect(() => {
    document.title = `Hello, ${name}`;
  }, [name]);

  return (
    <label>
      Name:{" "}
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
    </label>
  );
}

Example 2: Start and stop a timer

import { useEffect, useState } from "react";

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

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

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

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

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={() => setRunning(true)}>Start</button>
      <button onClick={() => setRunning(false)}>Stop</button>
      <button onClick={() => setSeconds(0)}>Reset</button>
    </div>
  );
}

Example 3: Subscribe to a browser event

import { useEffect, useState } from "react";

export default function OnlineStatus() {
  const [online, setOnline] = useState(() => navigator.onLine);

  useEffect(() => {
    function onOnline() {
      setOnline(true);
    }

    function onOffline() {
      setOnline(false);
    }

    window.addEventListener("online", onOnline);
    window.addEventListener("offline", onOffline);

    return () => {
      window.removeEventListener("online", onOnline);
      window.removeEventListener("offline", onOffline);
    };
  }, []);

  return <p>{online ? "Online" : "Offline"}</p>;
}

Common mistakes and how to fix them

Mistake 1: Creating an infinite loop by updating state every render

What you might do:

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

Why it breaks:

The effect runs after render, updates state, which triggers another render, and repeats.

Fix: Add a dependency array and use a functional update when you only need “increment once” behavior.

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

Mistake 2: Missing dependencies (stale values inside the effect)

What you might do:

useEffect(() => {
  console.log("Query:", query);
}, []);

Why it breaks:

The effect captures the initial query, so later updates never reach it.

Fix: Include what you use.

useEffect(() => {
  console.log("Query:", query);
}, [query]);

Troubleshooting

  • If you see an effect firing twice in development, check React Strict Mode. React 18 can run effects twice in dev to surface unsafe side effects. Test a production build to confirm real behavior.
  • If you see “Maximum update depth exceeded,” look for an effect that updates state without a dependency array, or dependencies that change every render.
  • If you see “Can’t perform a React state update on an unmounted component,” add cleanup. Timers, listeners, subscriptions, and async work should stop on unmount.
  • If an effect reruns “for no reason,” check for unstable dependencies like inline objects and functions. Move them inside the effect, or memoize them with useMemo or useCallback.

Quick recap

  • Put side effects inside useEffect(() => { ... }).
  • Use [] for mount-only effects, and [deps] to rerun when values change.
  • Return a cleanup function to stop timers, listeners, and subscriptions.
  • Include every value you read inside the effect in the dependency array.
  • Watch for infinite loops and Strict Mode double-runs in development