Skip to content
QSWEQB
easyConceptEntryMid

Why does my useEffect run twice on mount?

In development, Strict Mode deliberately runs one extra setup-cleanup-setup cycle for every effect so that missing cleanup becomes visible; it does not happen in production builds, and the fix is to write the cleanup the effect was missing rather than to guard the effect or remove Strict Mode.

5 min readUpdated 2026-07-26

What the interviewer is scoring

  • Whether you identify this as development-only Strict Mode behaviour instead of describing it as a React bug
  • Does the candidate read the double run as a diagnostic that has found something, rather than as noise to suppress
  • That they reach for a cleanup function before reaching for a ref guard or a module-level flag
  • Whether the cleanup they write genuinely undoes the setup, including cancelling in-flight async work
  • Can they say when an empty dependency array is the actual defect rather than the cure

Answer

Why the second run happens

Inside <StrictMode>, and only in development, React runs an extra setup-and-cleanup cycle before the real one. The sequence is setup, cleanup, setup: React mounts the component, runs your effect, simulates an unmount by running the cleanup, then simulates a remount with the previous state and runs the effect again. Nothing about your dependency array is wrong and nothing is rendering twice by accident. React is running a stress test on your behalf.

The test has a specific target: effects whose cleanup does not mirror their setup. An effect that opens a subscription without closing it, starts an interval without clearing it, adds a document-level listener without removing it, or pushes onto a shared array without removing its own entry will misbehave visibly under a doubled cycle. Without Strict Mode those defects surface much later and much further from their cause, as a slow memory leak, an event handler firing twice, or a chat connection the user cannot close.

React 18 introduced this behaviour, and it was not introduced to catch bugs for their own sake. It was preparation for features that add and remove parts of the UI while preserving state, so that a component can be unmounted and remounted with its state intact. Any code that assumes its effect runs exactly once per component lifetime breaks under that model, and Strict Mode makes the assumption fail immediately rather than at some point in the future. React 19 extended the same discipline to callback refs, which can now return a cleanup function of their own and get the same extra setup-and-cleanup cycle in development.

It does not happen in production

The doubled invocation is stripped from production builds. This matters for two reasons in an interview. First, a candidate who says "so my API is being called twice in production and I need to fix that" has misread the symptom, and the interviewer will usually probe until that is resolved. Second, it means the double run costs your users nothing, which removes the only real argument for suppressing it.

What does happen in production is the underlying race the double run exposes. Two effect runs in quick succession is exactly what a real user produces by navigating from user 7 to user 12 before the first request resolves. Strict Mode is not inventing an artificial scenario; it is putting a common one in front of you on the first render.

Cleanup, not a guard

Because the symptom is a duplicate call, the tempting fixes are the ones that stop the second call: a useRef set to true after the first run, a module-level hasRun variable, or deleting <StrictMode> from the entry file. All three make the console quiet and leave the defect exactly where it was, which is why an interviewer treats them as a negative signal rather than a working answer. A ref guard in particular is worse than doing nothing, because it hard-codes the assumption that the effect runs once per component lifetime, and that is the assumption the double invocation exists to break.

The correct move is to write the cleanup the effect was missing. The rule to state out loud is that a user should not be able to distinguish setup from setup-cleanup-setup. If they can, the cleanup is incomplete, and the fix is in the cleanup rather than in the number of times it runs.

Cancelling in-flight work

Fetching is the case where "undo the setup" is least obvious, because you cannot un-send a request. What you can do is make the response from an obsolete run unable to affect state.

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

  fetch(`/api/users/${id}`, { signal: controller.signal })
    .then((res) => res.json())
    .then(setUser)
    .catch((err) => {
      // An aborted request rejects. Swallow that one case, or every
      // cleanup will surface as an error in your UI.
      if (err.name !== "AbortError") setError(err);
    });

  return () => controller.abort();
}, [id]);

Without the abort, both requests stay in flight and whichever resolves last writes to state, so a slow response for a stale id can overwrite a fast response for the current one. If you cannot cancel the work, the lighter variant is a boolean captured in the closure that the cleanup sets, checked before calling setUser — it does not save the bandwidth, but it does make the stale result inert.

When the empty dependency array is the real bug

Candidates who have been burned by re-running effects often reach for [] as a way to force a single run, and that is the second defect this question tends to uncover. An empty array does not mean "run once"; it means "nothing this effect reads can ever change". The closure captures the values from the first render permanently, so an effect that reads a prop, a piece of state, or a function defined in the component body will keep using the values it saw on mount.

// Broken: the interval closes over the first render's `query` for ever.
useEffect(() => {
  const id = setInterval(() => search(query), 5000);
  return () => clearInterval(id);
}, []); // `query` is read inside, so it belongs here

The test is mechanical rather than a judgement call: list every reactive value the effect body reads and make sure each one appears in the array. If including a value causes an unacceptable amount of re-running, the answer is to restructure — move the work into an event handler, extract it outside the component, or hold the changing value in a ref deliberately — not to omit the dependency and hope. Omitting it converts a visible re-run into an invisible stale value, which is the harder bug of the two to find.

Strict Mode does not create the double invocation as a hazard; it reveals that your effect was never safe to run twice, and the only fix that survives production is a cleanup that undoes what the setup did.

Likely follow-ups

  • How would you cancel an in-flight fetch in the cleanup function, and what happens to the rejected promise?
  • When is an empty dependency array the wrong choice, and how do you tell?
  • Why did React 18 introduce this behaviour, and what future feature was it preparing for?
  • Which of this work belongs in an effect at all, versus in an event handler or a data-fetching library?

Related questions

Further reading

reactuseeffectstrict-modehookscleanup