React Hooks Series

useEffect

Run side-effects after render, fetch data, set up subscriptions, manipulate the DOM. The dependency array controls exactly when your effect fires.

01 — What it is

01
The signature
useEffect(() => {
  // side effect here

  return () => {
    // cleanup (optional) — runs before next effect or unmount
  };
}, [dependencies]);
The dependency array controls when the effect runs. Empty [] = run once on mount. No array = run after every render. Listed deps = run when those values change.
02
The three dependency array patterns
// 1. Run once on mount
useEffect(() => { fetchData(); }, []);

// 2. Run when 'id' changes
useEffect(() => { fetchUser(id); }, [id]);

// 3. Run after every render (rarely needed)
useEffect(() => { document.title = count; });

02 — Common patterns

03
Data fetching

Fetch on mount with an empty dependency array — the most common pattern.

useEffect(() => {
  fetch("https://api.example.com/data")
    .then((res) => res.json())
    .then((data) => setData(data));
}, []); // ← empty array = run once
04
Cleanup — avoiding memory leaks

Return a cleanup function to cancel subscriptions, clear timers, or abort fetches when the component unmounts or before the effect re-runs.

useEffect(() => {
  const controller = new AbortController();

  fetch("/api/data", { signal: controller.signal })
    .then((res) => res.json())
    .then(setData);

  return () => controller.abort(); // cleanup
}, []);
05
Gotcha — stale closures in deps

If you use a value inside an effect but don't list it as a dependency, the effect captures its initial value and never updates. Always include everything you read.

// ✗ stale — 'count' is always 0 inside the effect
useEffect(() => {
  console.log(count);
}, []);

// ✓ correct — runs whenever count changes
useEffect(() => {
  console.log(count);
}, [count]);

03 — Live demo

EffectTutorial — interactive

The fetch runs once on mount (empty deps). The counter increments independently — notice the fetch doesn't re-run when you click.

Fetching…
0

Counter re-renders the component — the fetch does not re-run (empty deps [])

Next: useRef — persist values and access DOM nodes without triggering a re-render.