If you upgraded to React 18 and started seeing your useEffect body run twice in development, you’re not imagining it — and it’s intentional.

What’s happening

React 18 mounts, unmounts, then remounts every component once in development (Strict Mode) to surface side-effect bugs. That means effects run, get cleaned up, then run again.

useEffect(() => {
  const id = setInterval(() => console.log("tick"), 1000);
  return () => clearInterval(id); // runs between the two invocations
}, []);

Why it’s a good thing

  • It forces you to write effects that are safe to run, clean up, and re-run — the real behavior in production across fast refresh and concurrent features.
  • Most “double run” bugs are actually missing cleanup bugs that would bite you later.

How to fix the common symptoms

If a fetch fires twice and you see duplicate requests, make sure you cancel the in-flight request:

useEffect(() => {
  const controller = new AbortController();
  fetch("/api/user", { signal: controller.signal })
    .then((r) => r.json())
    .then(setUser)
    .catch((e) => { if (e.name !== "AbortError") throw e; });
  return () => controller.abort();
}, []);

Don’t suppress it

Avoid hacks like a module-level “hasRun” flag. That hides the symptom and breaks under concurrent rendering. The double-invoke only happens in development — production builds run effects exactly once.