useEffect: Dependencies, Cleanup, and Common Traps

When effects run, how dependency arrays work, and how to avoid stale closures.

F

Frontend Interview Team

February 08, 2026

3 min read
useEffect: Dependencies, Cleanup, and Common Traps

30‑second interview answer

useEffect lets you run side effects after React commits updates to the DOM—things like data fetching, subscriptions, timers, and manual DOM work. The dependency array controls when the effect runs. Cleanup runs before the next effect (and on unmount) to avoid leaks. Most bugs come from incorrect dependencies and stale closures.


When does useEffect run?

Typical mental model:

  • Render happens (React calls components)
  • Commit happens (DOM updates)
  • Then effects run (after paint, generally)

So useEffect is the right place for work that should not block rendering.

useLayoutEffect vs useEffect

  • useLayoutEffect: runs after DOM mutations but before paint (can block paint)
  • useEffect: runs after paint (better for most cases)

In interviews:

“Prefer useEffect unless you must measure layout before paint.”


Dependency array: what it really means

1) No dependency array

useEffect(() => {
  // runs after every commit
});

2) Empty dependency array []

useEffect(() => {
  // run once on mount
  return () => {
    // cleanup on unmount
  };
}, []);

3) With dependencies

useEffect(() => {
  // runs when any dependency changes
}, [userId, token]);

Rule: your deps should include everything from render scope that the effect reads.


Cleanup (critical for interviews)

Cleanup runs:

  • before the effect re-runs
  • when the component unmounts

Typical patterns:

Subscriptions

useEffect(() => {
  const unsub = store.subscribe(() => {
    // ...
  });
  return () => unsub();
}, []);

Timers

useEffect(() => {
  const id = setInterval(() => {
    // ...
  }, 1000);
  return () => clearInterval(id);
}, []);

The most common bug: stale closures

This happens when your effect “captures” old state/props.

Example:

function Example() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // ❌ stale value
    }, 1000);
 
    return () => clearInterval(id);
  }, []); // ❌ count missing
}

Fix 1: include dependencies

useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, [count]);

Fix 2: use functional updates / refs

useEffect(() => {
  const id = setInterval(() => {
    setCount((c) => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

Or store mutable latest value in a ref:

const latest = useRef(count);
useEffect(() => {
  latest.current = count;
}, [count]);

Fetching data safely

Basic fetch

useEffect(() => {
  let cancelled = false;
 
  (async () => {
    const res = await fetch(`/api/user/${userId}`);
    const data = await res.json();
    if (!cancelled) setUser(data);
  })();
 
  return () => {
    cancelled = true;
  };
}, [userId]);

Better: use AbortController

useEffect(() => {
  const controller = new AbortController();
 
  (async () => {
    try {
      const res = await fetch(url, { signal: controller.signal });
      const data = await res.json();
      setData(data);
    } catch (e) {
      if (e.name !== 'AbortError') throw e;
    }
  })();
 
  return () => controller.abort();
}, [url]);

React 18 Strict Mode (why effects run twice in dev)

In development, React may intentionally run certain lifecycles twice to help you find unsafe effects.

Interview-ready line:

“In dev Strict Mode, effects can mount/unmount twice. Write effects to be resilient and clean up properly.”


Anti-patterns to avoid

  1. Putting async directly in effect callback
useEffect(async () => {}) // ❌

Do this instead:

useEffect(() => {
  async function run() {}
  run();
}, []);
  1. Disabling dependency lint rule blindly

If ESLint says you need deps, you usually do.

  1. Doing heavy work in effects on every keystroke

Debounce or move work elsewhere.


Mini Q&A

Q1: When does cleanup run?

  • Before next run and on unmount.

Q2: Why does missing deps cause bugs?

  • The effect closes over old values.

Q3: When to use useLayoutEffect?

  • When you must measure layout before paint.

Summary checklist

  • I can explain dependency array meaning.
  • I know cleanup timing.
  • I can explain stale closure with an example.
  • I understand Strict Mode double-invoke in dev.