useEffect runs twice on mount in development
appears when:In development when React 18+ Strict Mode intentionally mounts, unmounts, and remounts every component to surface unsafe effects
React Strict Mode double render useEffect
This is not a bug. React runs effects twice in dev specifically to catch the ones that cannot handle it. Fix the effect, not the warning.
AbortController to fetches, removeEventListener in cleanup, cancel timers with clearTimeout, unsubscribe from subscriptions. Disabling Strict Mode hides bugs that also manifest in production under different conditions.Quick fix for Strict Mode double render
01"use client";02import { useEffect, useState } from "react";03 04export function UserCard({ id }: { id: string }) {05 const [user, setUser] = useState<User | null>(null);06 07 useEffect(() => {08 const controller = new AbortController();09 10 fetch(`/api/users/${id}`, { signal: controller.signal })11 .then((r) => r.json())12 .then((data) => setUser(data))13 .catch((err) => {14 if (err.name === "AbortError") return; // strict mode double-mount — expected15 console.error(err);16 });17 18 // Cleanup — runs between the double-mount and on real unmount19 return () => controller.abort();20 }, [id]);21 22 if (!user) return <Spinner />;23 return <article>{user.name}</article>;24}Deeper fixes when the quick fix fails
01 · Event listener cleanup pattern
01useEffect(() => {02 const onScroll = () => setScrolled(window.scrollY > 100);03 window.addEventListener("scroll", onScroll, { passive: true });04 return () => window.removeEventListener("scroll", onScroll);05}, []);06 07// Without the cleanup, every remount adds another listener.08// In Strict Mode dev you get 2 listeners firing per scroll.09// In production, every mount/unmount cycle (route changes) leaks one.02 · Deduplicate single-fire effects with a ref
01"use client";02import { useEffect, useRef } from "react";03 04export function usePageView(path: string) {05 const sent = useRef(false);06 07 useEffect(() => {08 if (sent.current) return; // ignore double-mount09 sent.current = true;10 track("page_view", { path });11 }, [path]);12}13 14// Better: move tracking to a route listener in app/layout.tsx15// so it fires once per navigation, not per component mount.03 · Replace raw useEffect fetches with React Query
01"use client";02import { useQuery } from "@tanstack/react-query";03 04export function useUser(id: string) {05 return useQuery({06 queryKey: ["user", id],07 queryFn: async ({ signal }) => {08 const res = await fetch(`/api/users/${id}`, { signal });09 if (!res.ok) throw new Error(`status ${res.status}`);10 return res.json() as Promise<User>;11 },12 });13}14 15// React Query handles:16// - AbortController automatically17// - dedup across double-mount18// - refetch on window focus (or not, your call)19// - stale-while-revalidate cachingWhy AI-built apps hit Strict Mode double render
React 18 shipped a developer-only mode where every component mounts, unmounts, and remounts on first render when wrapped in <StrictMode>. In the Next.js App Router this is enabled by default. The mechanism is deliberate: by invoking the full unmount/remount cycle, React forces effects to exercise their cleanup paths immediately rather than hiding problems until a real navigation or unmount occurs in production. If your effect subscribes to something on mount and does not unsubscribe on unmount, Strict Mode reveals the leak by showing two subscriptions active simultaneously.
AI-generated React code trips on this constantly. A Lovable or Bolt scaffold reaches for useEffect as the default place to do anything on component load — fetching data, starting timers, registering listeners, firing analytics. The generated code rarely includes a cleanup function because tutorials the model was trained on often omitted them. In production the effect runs once and appears fine. In Strict Mode the effect runs twice, exposing the missing cleanup. Developers see two API calls in the network tab, assume the tool is broken, and disable Strict Mode — which masks the same leak they will eventually hit in production when users navigate away mid-request.
The three most common unsafe patterns: fetch without an AbortController, addEventListener without a matching removeEventListener, and setInterval without clearInterval. Each has a cleanup shape: return a function from the effect that reverses the setup. For fetch, return () => controller.abort(). For listeners, return () => element.removeEventListener(...). For intervals, return () => clearInterval(id).
A subtler trap is analytics or tracking side effects. These often should not live in useEffect at all. Page-view tracking belongs in a route-change listener that fires once per navigation. Click tracking belongs in the onClick handler. Putting them in an effect makes them fire twice in dev and frequently in prod when the component remounts for reasons unrelated to user action.
Strict Mode double render by AI builder
How often each AI builder ships this error and the pattern that produces it.
| Builder | Frequency | Pattern |
|---|---|---|
| Lovable | Every data fetch | Raw useEffect + fetch with no AbortController or cleanup |
| Bolt.new | Common | Disables Strict Mode to hide double-render instead of fixing effects |
| Cursor | Common | Analytics tracking in useEffect instead of event handler — double-fires |
| Base44 | Sometimes | Event listeners in useEffect with no removeEventListener in cleanup |
| Replit Agent | Rare | setInterval in useEffect, no clearInterval — accumulates timers on rerender |
Related errors we fix
Stop Strict Mode double render recurring in AI-built apps
- →Never disable React Strict Mode — the double-render is free bug detection in dev.
- →Every fetch in useEffect needs an AbortController passed as signal and aborted in cleanup.
- →Use React Query or SWR for any data fetching beyond trivial cases — they handle dedup for you.
- →Move page-view analytics to a route change listener, not a component useEffect.
- →Write a cleanup return for every effect that starts something — subscriptions, listeners, timers.
Still stuck with Strict Mode double render?
Strict Mode double render questions
Why does useEffect run twice in React development?+
Should I disable React Strict Mode to stop the double render?+
How do I make a fetch in useEffect idempotent?+
Why does my analytics event fire twice in development?+
How long does an Afterbuild Labs React audit take?+
Ship the fix. Keep the fix.
Emergency Triage restores service in 48 hours. Break the Fix Loop rebuilds CI so this error cannot ship again.
Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.