Why your React effect runs twice in dev

Troubleshooting 7 min read Updated August 2026

You add a useEffect that fetches data, open the network tab, and see two identical requests. Nothing's broken — this is React's StrictMode doing its job in development. But it's pointing at something worth fixing, so don't just reach for the off switch.

What's actually happening

In development, <StrictMode> deliberately mounts each component, unmounts it, then mounts it again. Every effect therefore runs, cleans up, and runs a second time:

mounteffect runs        (request #1)
unmountcleanup runs
remounteffect runs again  (request #2)

This is on purpose. React is simulating what happens when a component is removed and re-added for real — on navigation, on a list re-order, on a key change — to catch effects that don't clean up after themselves. In production, the effect runs once.

Why React does this

An effect that can't survive being run twice is an effect with a latent bug. If two mounts leak two subscriptions, or fire two requests where the second's response can arrive first, that same bug will hit in production the moment the component genuinely remounts. StrictMode just makes it happen every time in dev, where you'll notice.

The fix is almost never "run it once." It's "make the effect safe to run, undo, and run again" — which is what cleanup is for.

The real fix: cleanup

Every effect that starts something ongoing — a request, a timer, a subscription — should return a function that stops it. For fetches, that means aborting the stale request:

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

  fetch(`/api/users/${id}`, { signal: controller.signal })
    .then((r) => r.json())
    .then(setUser)
    .catch((err) => {
      if (err.name !== "AbortError") throw err   // ignore the intended abort
    })

  return () => controller.abort()   // cleanup: cancel the in-flight request
}, [id])

Now the StrictMode sequence is harmless: the first request is aborted by cleanup before the second runs. This also fixes a real race condition — when id changes quickly, an older, slower response can no longer overwrite a newer one.

Practice this
Practice effects and data fetching
Work through React challenges with real cleanup, races, and async — graded automatically.
Practice React

Cleanup patterns for other effects

The same shape applies to anything an effect sets up:

  • Timersconst id = setTimeout(...); return () => clearTimeout(id).
  • Event listenersaddEventListener in the effect; removeEventListener in cleanup.
  • Subscriptions — subscribe in the effect; return the unsubscribe function.
  • Intervals and observers — clear the interval or disconnect() the observer.

When it's genuinely a one-time action

Some effects shouldn't repeat — logging an analytics event, say. Even then, resist disabling StrictMode. Prefer:

  • Moving the action out of render entirely — trigger it from the event that causes it (a click), not from an effect.
  • A ref guard as a last resort, if the action truly must fire once per real mount.

Removing <StrictMode> makes the two requests disappear, but it also removes the check that would have caught the leak. Keep it on, add the cleanup, and the double run stops being a symptom and starts being a free correctness test.

FAQ

Why does my useEffect run twice?

In development, React StrictMode intentionally mounts, unmounts, and remounts each component once, so every effect runs → cleans up → runs again. It's a deliberate check for missing cleanup, not a bug. In production the effect runs once.

How do I stop useEffect from running twice?

You usually shouldn't. The double-invoke is surfacing a real problem: an effect without proper cleanup. Add a cleanup function that cancels the work, and the double run becomes harmless. Removing StrictMode hides the symptom, not the cause.

Does the double render happen in production?

No. StrictMode's double-invocation only happens in development builds. But an effect that misbehaves under it — duplicate requests, leaked subscriptions — will also misbehave in production when a component remounts for real.

Put it into practice

Solve real challenges in a full browser IDE — graded automatically.

Browse frameworks