afterbuild/ops
ERR-685/React · Effects
ERR-685
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.

Last updated 17 April 2026 · 6 min read · By Hyder Shah
Direct answer
React 18 Strict Mode mounts every component twice in dev to surface effects that are not idempotent. Make your effects safe to run twice: add 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

components/UserCard.tsx
typescript
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}
Canonical idempotent effect — AbortController handles the double-mount cleanly, production sees one fetch

Deeper fixes when the quick fix fails

01 · Event listener cleanup pattern

hooks/use-scroll.ts
typescript
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.
Always return a cleanup from effects that register listeners

02 · Deduplicate single-fire effects with a ref

hooks/use-page-view.ts
typescript
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.
Ref guard — works but usually indicates the effect should not be in a component

03 · Replace raw useEffect fetches with React Query

hooks/use-user.ts
typescript
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 caching
Any app with 3+ API calls should use React Query or SWR — useEffect fetches do not scale

Why 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.

AI builder × Strict Mode double render
BuilderFrequencyPattern
LovableEvery data fetchRaw useEffect + fetch with no AbortController or cleanup
Bolt.newCommonDisables Strict Mode to hide double-render instead of fixing effects
CursorCommonAnalytics tracking in useEffect instead of event handler — double-fires
Base44SometimesEvent listeners in useEffect with no removeEventListener in cleanup
Replit AgentRaresetInterval in useEffect, no clearInterval — accumulates timers on rerender

Related errors we fix

Stop Strict Mode double render recurring in AI-built apps

Still stuck with Strict Mode double render?

Emergency triage · $299 · 48h turnaround
We restore service and write the root-cause report.
start the triage →

Strict Mode double render questions

Why does useEffect run twice in React development?+
React 18 introduced a development-only behavior where every effect mounts, unmounts, and mounts again. The purpose is to surface effects that do not handle unmount cleanly — memory leaks, duplicate listeners, lingering subscriptions, repeated analytics calls. Production runs effects exactly once. If your app breaks because an effect runs twice in dev, production is also broken — it just fails differently (e.g. stale listeners accumulating over re-renders, leaked subscriptions, double-fired analytics).
Should I disable React Strict Mode to stop the double render?+
No. Disabling Strict Mode hides symptoms of real bugs without fixing them. The double-render is a test harness, not a defect. Every effect your app runs should be idempotent — safe to run twice. If it is not, either the effect needs a cleanup function (unsubscribe, abort, clear) or the side effect belongs somewhere else entirely (event handler, server action, page load). Disabling Strict Mode in dev means shipping unsafe effects to prod where they cause harder-to-debug issues.
How do I make a fetch in useEffect idempotent?+
Use AbortController. Inside the effect, create a controller, pass its signal to fetch, and abort in the cleanup return. When the effect runs twice, the first fetch aborts before the second begins, so only one request reaches the server and state updates only once. For SWR or React Query, the library handles this for you — prefer those over raw useEffect fetches in any app with more than 3 API calls.
Why does my analytics event fire twice in development?+
Because you put the event inside useEffect without a guard. Strict Mode renders the component, unmounts, and remounts — your effect fires, cleanup runs, then the effect fires again. For analytics, the right fix is usually not inside useEffect at all: move tracking to event handlers (onClick, form submit) or to a route-change listener that fires once per navigation. If the event must be in an effect, use a ref to deduplicate: if (sent.current) return; sent.current = true; track().
How long does an Afterbuild Labs React audit take?+
For a medium React app (50-100 components, 20-30 effects), auditing every effect for Strict Mode compatibility takes 2-3 hours. Our Break the Fix Loop service includes: effect-level review, adding AbortControllers and cleanup where missing, migrating duplicate-prone effects to React Query, and a test harness that runs every page under Strict Mode to catch regressions.
Next step

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.

About the author

Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.

Sources