[{"data":1,"prerenderedAt":6},["ShallowReactive",2],{"post-content-react-useeffect-runs-twice-strict-mode":3},{"content":4,"lastModified":5},"\u003Cp>\u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1461749280684-dccba630e2f6?q=80&amp;w=1200&amp;auto=format&amp;fit=crop\" alt=\"JavaScript code on a laptop screen during debugging\" loading=\"lazy\" width=\"1200\" height=\"800\">\u003C\u002Fp>\n\n\u003Cp>Last month our signup analytics showed exactly double the signups we actually had. The culprit was a \u003Ccode>useEffect\u003C\u002Fcode> that fired its tracking call twice on every page load in development, a teammate \"fixed\" it by deleting \u003Ccode>&lt;StrictMode&gt;\u003C\u002Fcode> from the root, and we shipped for three days before anyone noticed the real problem: a different effect with a broken dependency array was firing on every render in production. Removing StrictMode hid the symptom and kept the bug.\u003C\u002Fp>\n\n\u003Cp>If your effects run twice, here is what is actually going on and the fixes that hold up.\u003C\u002Fp>\n\n\u003Ch2>Why the effect fires twice\u003C\u002Fh2>\n\n\u003Cp>Since React 18, StrictMode mounts every component, runs its effects, runs the cleanup, then runs the effects again. On purpose. The sequence in development looks like this:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-jsx\">\u002F\u002F 1. component mounts, effect runs\n\u002F\u002F 2. React simulates an unmount, cleanup runs\n\u002F\u002F 3. React remounts, effect runs again\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>It only happens in development builds. In production your effects run once. The point is to expose effects that assume they run exactly once, because those are the same effects that break for real when a component unmounts and remounts, or when the user navigates away mid-request.\u003C\u002Fp>\n\n\u003Cp>So the double run is not the bug. It is the test. The bug is whatever breaks when the test runs.\u003C\u002Fp>\n\n\u003Ch2>Fetches: abort in the cleanup\u003C\u002Fh2>\n\n\u003Cp>The most common case is data fetching. Without a cleanup, StrictMode starts two requests and the stale one can win the race:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-jsx\">useEffect(() =&gt; {\n  fetch('\u002Fapi\u002Fusers')\n    .then((r) =&gt; r.json())\n    .then(setUsers);\n}, []);\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Abort the request when the effect tears down and only one fetch ever completes:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-jsx\">useEffect(() =&gt; {\n  const controller = new AbortController();\n\n  fetch('\u002Fapi\u002Fusers', { signal: controller.signal })\n    .then((r) =&gt; r.json())\n    .then(setUsers)\n    .catch((err) =&gt; {\n      if (err.name !== 'AbortError') setError(err);\n    });\n\n  return () =&gt; controller.abort();\n}, []);\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>StrictMode runs: fetch starts, cleanup aborts it, effect runs again, fresh fetch completes. One request in flight at a time. I covered the messier corners of this in my \u003Ca href=\"\u002Fposts\u002Fcanceling-fetch-abortcontroller\u002F\">AbortController deep dive\u003C\u002Fa>, including why aborted rejections land where they do.\u003C\u002Fp>\n\n\u003Ch2>Subscriptions and timers: the same move\u003C\u002Fh2>\n\n\u003Cp>Anything that registers itself needs to unregister itself. WebSocket, event listener, store subscription, interval. All the same shape:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-jsx\">useEffect(() =&gt; {\n  const ws = new WebSocket('wss:\u002F\u002Fexample.com\u002Ffeed');\n  ws.onmessage = (e) =&gt; setMessages((prev) =&gt; [...prev, e.data]);\n  return () =&gt; ws.close();\n}, []);\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Without that \u003Ccode>ws.close()\u003C\u002Fcode>, StrictMode leaves you with two open connections in dev, and your cleanup hygiene is broken everywhere else too. Honestly, this is the check I run first on any codebase I inherit: open a page, watch the Network tab, count the duplicate sockets and subscriptions.\u003C\u002Fp>\n\n\u003Ch2>What about things that genuinely run once?\u003C\u002Fh2>\n\n\u003Cp>Analytics is the classic case. You do not want to abort a page-view event, and you do not want to fire it twice either. A ref guard works:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-jsx\">const tracked = useRef(false);\n\nuseEffect(() =&gt; {\n  if (tracked.current) return;\n  tracked.current = true;\n  analytics.page('dashboard');\n}, []);\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>One warning. Do not reach for a module-level \u003Ccode>let sent = false\u003C\u002Fcode> instead. A ref belongs to one component instance, so a remount gets a fresh one. A module-level flag does not reset, which means it can keep swallowing runs after a hot reload, and tracking quietly stops until you do a full reload. I have paid for that lesson twice.\u003C\u002Fp>\n\n\u003Ch2>When it really is a bug: unstable dependencies\u003C\u002Fh2>\n\n\u003Cp>Here is the part the StrictMode articles skip. If effects run twice in a production build, StrictMode is not the cause, because it does nothing there. The usual suspect is an object or function in the dependency array that gets a new reference every render:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-jsx\">useEffect(() =&gt; {\n  fetchData(config);\n}, [config]); \u002F\u002F new object every render, effect runs every render\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>React compares dependencies by reference. A new object means \"changed\", every time. Pass primitives instead:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-jsx\">useEffect(() =&gt; {\n  fetchData({ page, limit });\n}, [page, limit]);\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>That was exactly our double-signup bug in production. Dev double-fired because of StrictMode, prod fired constantly because of the dependency array. Two different causes wearing the same costume.\u003C\u002Fp>\n\n\u003Cp>So before you delete StrictMode from the root: build for production, run the app, and check whether the effect still misbehaves. Once in prod means the effect needs a cleanup. Twice in prod means the dependencies are lying to you. \u003Ca href=\"\u002Fposts\u002Freact-context-re-render-why-memo-doesnt-help\u002F\">The reference-comparison problem shows up in re-renders too\u003C\u002Fa>, and it is the same mental model in both places.\u003C\u002Fp>\n\n\u003Cp>StrictMode caught a real bug for us. Keep it on, write effects that survive running twice, and the double-fire disappears on its own.\u003C\u002Fp>\n","2026-10-20",1792462273083]