You add a console.log inside useEffect, refresh, and see it printed twice. Your API call fires twice. Your analytics event fires twice.
Quick answer: In development, React Strict Mode intentionally mounts, unmounts and remounts every component once to check that your effects clean up properly. It does not happen in production.
Is it a bug?
No. It's a stress test. React is asking: "If this component gets unmounted and mounted again, does your effect still behave?" In future React features (like preserving state while hidden), that will genuinely happen.
Check your main.jsx or Next.js config:
<React.StrictMode>
<App />
</React.StrictMode>Build for production and the double run disappears.
Don't "fix" it by removing Strict Mode
That hides the symptom. The real question is: why does running twice cause a problem? Usually because the effect is missing a cleanup.
The real fix: write a cleanup
Subscriptions and listeners
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);Without the cleanup, the second run adds a duplicate listener.
Timers
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);Fetching data
Cancel the first request, so only the last one counts:
useEffect(() => {
const controller = new AbortController();
fetch("/api/user", { signal: controller.signal })
.then((res) => res.json())
.then(setUser)
.catch((err) => {
if (err.name !== "AbortError") console.error(err);
});
return () => controller.abort();
}, []);What about things that must run only once?
Things like "send a purchase event" shouldn't live in an effect at all. Put them where the action actually happens:
// ❌ Fires on mount (and twice in dev)
useEffect(() => {
trackPurchase();
}, []);
// ✅ Fires when the user actually clicks
<button
onClick={() => {
buy();
trackPurchase();
}}
>
Buy
</button>;Effects are for synchronizing with something external, not for running one-time logic.
Avoid the useRef hack
You'll find advice like this:
const ran = useRef(false);
useEffect(() => {
if (ran.current) return;
ran.current = true;
doThing();
}, []);It silences the warning but keeps the underlying bug (no cleanup) and can break when React remounts intentionally. Prefer a proper cleanup.
Checklist
- ✅ Double effects only in dev + Strict Mode? Then it's expected.
- ✅ Every listener, timer and subscription has a cleanup function.
- ✅ Fetches use
AbortController(or a library like TanStack Query). - ✅ User actions are handled in event handlers, not effects.