Skip to content
React rc hooks 4 min read

useEffect

useEffect lets a component reach outside of React to synchronize with systems that React does not control — the browser DOM, network requests, timers, subscriptions, or third-party widgets. React’s job is to render UI from state; an effect is the escape hatch for everything else that has to happen as a result of that render. Mastering when an effect runs, what its dependency array means, and how cleanup works is the difference between code that quietly leaks subscriptions and code that stays in sync.

Effects run after the commit

When a component renders, React calculates the new UI and commits it to the DOM. Only after the browser has updated the screen does React run your effect. This timing is deliberate: the user sees the rendered output first, and side effects happen afterward without blocking the paint.

import { useState, useEffect } from "react";

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

  useEffect(() => {
    document.title = `Clicked ${count} times`;
    console.log("effect ran with count =", count);
  });

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

Output:

effect ran with count = 0
effect ran with count = 1
effect ran with count = 2

The effect runs once after the first render, then again after every render that changes count. With no dependency array at all, it re-runs after every commit.

The dependency array

The second argument to useEffect is the dependency array. It tells React which reactive values the effect reads, so React can decide whether to re-run it. React compares each dependency to its previous value with Object.is; if all of them are unchanged, the effect is skipped.

Second argumentWhen the effect runs
omittedAfter every render
[a, b]After the first render, then whenever a or b changes
[]Once, after the first render only
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let active = true;
    setUser(null);

    fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        if (active) setUser(data);
      });

    return () => {
      active = false;
    };
  }, [userId]);

  if (!user) return <p>Loading…</p>;
  return <h1>{user.name}</h1>;
}

Because userId is the only dependency, the fetch re-runs only when the prop changes — not on unrelated re-renders.

Every reactive value your effect reads (props, state, and values derived from them) must appear in the dependency array. The react-hooks/exhaustive-deps ESLint rule enforces this. Trimming the array to “fix” extra runs causes stale-closure bugs instead — change what the effect reads, not what you declare.

Cleanup functions

An effect can return a function. React calls it to clean up before the effect runs again, and once more when the component unmounts. This is how you tear down anything the effect set up: timers, event listeners, subscriptions, sockets.

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

  useEffect(() => {
    const onResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", onResize);

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

  return <p>Window is {width}px wide</p>;
}

The lifecycle for an effect with dependencies is: setup → (dependency changes) → cleanup → setup → … → cleanup on unmount. Pairing every subscription with a matching cleanup is what prevents memory leaks and duplicate listeners.

In development, React 18+ Strict Mode mounts each component twice on purpose — running setup, cleanup, then setup again — to surface effects that forgot to clean up. If your effect is correct, the double-invoke is invisible in behavior.

Common uses

  • Fetching data tied to a prop or state value (with an active flag or AbortController to ignore stale responses).
  • Subscribing to a store, WebSocket, or browser event, with cleanup that unsubscribes.
  • Imperative DOM work React doesn’t manage — focusing an input, syncing a <canvas>, integrating a chart library.
  • Timers via setInterval/setTimeout, cleaned up so they don’t fire after unmount.

You might not need an Effect

Effects are overused. A large class of code that people put in useEffect belongs elsewhere:

  • Transforming data for render — compute it during render (or with useMemo), not in an effect that writes to extra state.
  • Responding to a user event — put the logic in the event handler, where you know exactly what happened.
  • Resetting state when a prop changes — usually a key on the component does it more cleanly.
// ❌ Unnecessary effect to derive state
function Cart({ items }) {
  const [total, setTotal] = useState(0);
  useEffect(() => {
    setTotal(items.reduce((sum, i) => sum + i.price, 0));
  }, [items]);
  return <p>Total: {total}</p>;
}

// ✅ Just calculate during render
function Cart({ items }) {
  const total = items.reduce((sum, i) => sum + i.price, 0);
  return <p>Total: {total}</p>;
}

The deriving version triggers an extra render and can show a stale total for one frame. The plain calculation is simpler and always correct.

Best Practices

  • Use the narrowest dependency array that is still complete — never silence the linter by omitting deps.
  • Always return a cleanup function from effects that subscribe, time, or open anything.
  • Keep one concern per effect; split unrelated logic into separate useEffect calls.
  • For data fetching, guard against race conditions with an active flag or AbortController.
  • Ask “is this a side effect at all?” — derive values during render and handle user actions in event handlers instead.
  • Use useLayoutEffect only when you must measure or mutate the DOM before paint; otherwise prefer useEffect.
  • Treat Strict Mode’s double-run in development as a feature that exposes missing cleanup.
Last updated June 14, 2026
Was this helpful?