In a React 19 app, a component's useEffect with an empty dependency array runs twice when the component first mounts in development, but only once in the production build. What causes that, and what is it there for?
answer
- development-only wrapper near the root
- an extra mount, not an extra update
- setup, cleanup, setup
- missing cleanup made loud and early
- production builds run it once
basics
~20 sReact's <StrictMode> deliberately mounts each component an extra time in development, running setup, then cleanup, then setup again, so effects that never clean up break loudly. It is development-only; production mounts and runs the effect once.
solid answer
~40 sThe app is wrapped in `<StrictMode>`, which in development simulates an immediate unmount and remount of every component the first time it mounts. For effects that means React runs the setup function, runs the cleanup function it returned, then runs setup again. Nothing about your dependency array is wrong, and this does not happen in a production build — `<StrictMode>` compiles away to a passthrough there. The point is to make a missing or incomplete cleanup fail immediately in development instead of leaking in production, because React reserves the right to unmount and remount a component for real (Suspense, future features, or simply navigating away and back). The correct response is to write the cleanup that makes the effect survive being run twice, not to suppress the second run.
go deeper
Recognize the symptom on sight: an effect running twice on mount in development means the tree is wrapped in <StrictMode>. Say plainly that it is intentional, development-only, and that the fix is to return a cleanup function.
Explain the exact sequence React performs — setup, cleanup, setup — and why that ordering leaves a correctly written effect in the same state as a single run. Be able to show an interval or subscription effect that survives it.
Show that you treat the double run as a diagnostic signal. Walk through triaging what your codebase does twice, separating genuinely broken effects from external systems that merely need idempotent handling, and argue why the cleanup is the deliverable rather than a suppression.
Frame it as a class of correctness guarantee rather than a dev annoyance: React reserves the right to remount, so cleanup-completeness is a codebase-wide invariant. Be ready to say how you enforce it in review and how you would roll the check across a tree that currently fails it.
## What you are actually observing Somewhere near the root of the app there is a tree wrapped in React's `StrictMode` component: ```jsx import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; createRoot(document.getElementById('root')).render( <StrictMode> <App /> </StrictMode> ); ``` `StrictMode` is a development-only tool. It renders nothing itself and adds no DOM node; it turns on extra checks for everything inside it. One of those checks is that, on a component's **initial mount in development**, React deliberately performs an extra mount cycle for its effects: it runs the effect's setup function, then runs the cleanup function that setup returned, then runs setup a second time. Written out, the sequence is `setup → cleanup → setup`. That is why a `console.log` inside the effect prints twice, why a counter incremented in an effect lands on 2, and why a subscription created in an effect appears to exist twice. ## Why React does this on purpose An effect describes a *synchronization*: it connects the component to something outside React, and its cleanup disconnects it again. React's contract is that a correctly written effect can be set up and torn down any number of times without the app drifting into a broken state. In production React genuinely does mount and unmount components repeatedly — the user navigates away and back, a parent re-keys a subtree, a conditional branch flips. The trouble is that a *missing* cleanup usually looks fine on the first mount. Bugs from it appear later, in production, as duplicated subscriptions, doubled event listeners, growing memory, or timers that keep firing after their component is gone. StrictMode's extra mount forces exactly that failure to happen on the very first render on the developer's own machine, where it is cheap to notice and cheap to fix. ## What "correctly written" looks like An effect passes the StrictMode check when its cleanup fully undoes its setup: ```jsx useEffect(() => { const id = setInterval(() => tick(), 1000); return () => clearInterval(id); }, []); ``` Run setup twice with the cleanup in between and exactly one interval is alive — the first interval was cleared before the second one was created. Delete the `return` line and you have two intervals running forever, which is precisely the bug StrictMode is designed to surface. The same shape applies to any external connection: add a listener, remove it in cleanup; open a socket, close it in cleanup; start an observer, disconnect it in cleanup. ## What it does not mean It is not a React bug, and it is not caused by a mistaken dependency array. An empty array still means "synchronize once per mount"; StrictMode is simulating an extra mount, not an extra update. It is also not a performance problem in your users' browsers: production builds do not double-invoke anything, so measuring the effect of StrictMode on production timings is meaningless. Crucially, the behaviour is a *symptom detector*, not the disease. Two visible consequences are possible when your effect runs twice: - Nothing observable changes, because the cleanup undid the first run. The effect is correct. - Something observable happens twice — two connections, two rows, two log lines that never go away. The effect is incomplete, and the second run has just told you so. ## How to respond Write the cleanup. If the effect has genuinely nothing to undo, ask whether it belongs in an effect at all — work that must happen once *in response to something a user did* usually belongs in the event handler that handled the interaction rather than in an effect that fires on mount. Removing `<StrictMode>` to silence the double run is the classic wrong move: it does not fix anything, it only hides the report. The double invocation is temporary noise in development; the missing cleanup it exposes is permanent damage in production. ## Turning it off, legitimately StrictMode wraps a *subtree*, not necessarily the whole app, so a team migrating a large codebase can enable it around the parts that are ready and expand outward. That is a scoping decision, not an escape hatch: the goal is still to have every part of the tree under it eventually.
- If I delete <StrictMode> from the app, does the underlying problem go away?No — you only stop being told about it. The double mount is a detector, not a cause. Any effect that leaks under StrictMode leaks in production too, as soon as the component is genuinely unmounted and remounted: navigating away and back, a parent changing its key, or a conditional branch flipping. The visible symptom moves from your console to your users' sessions.
- Does the double run affect production performance?No. `<StrictMode>` performs its extra invocations only in development builds; in production it renders its children and does nothing else. So an effect that runs twice locally runs once for real users, and any timing you measure in development under StrictMode is not representative of production.
- Is this the same thing as an effect firing repeatedly because of a changing dependency?No, and confusing them wastes debugging time. StrictMode produces exactly one extra mount cycle at initial mount — setup, cleanup, setup — and then stops. An effect that keeps re-firing on every render is a dependency problem: a value in the array is a new object or function each render. The tell is that StrictMode's doubling stops after mount, while a dependency problem does not.
saying these in an interview costs you the question
- Calls the double effect run a React bug
- Says effects also run twice in production
- Blames the dependency array for the extra run
- Deletes StrictMode so the symptom disappears
- Thinks it is an extra update rather than an extra mount