Why does StrictMode behave differently in development?
StrictMode intentionally double-invokes components, effects, and reducers in development to surface impure renders and effect cleanup bugs that would break under concurrent rendering.
StrictMode is a development-only opt-in wrapper (<React.StrictMode>...) that activates extra correctness checks. It is silent in production builds — Next.js enables it by default in dev, but the checks are only there to surface bugs you'd otherwise hit as production heisenbugs once concurrent features (transitions, Suspense, offscreen) start interrupting and replaying renders.
The behaviors that confuse developers in React 18+:
- Components render twice on mount. React calls your component function, throws the result away, then calls it again. If your render is pure (just takes props/state → returns JSX), both outputs are identical and the second one is committed. If your render has side effects — pushing to a module-level array, incrementing a counter, calling
fetchat render time, mutating props — those side effects happen twice and you'll see them. That's the point: under concurrent rendering, React is allowed to render a component multiple times before committing (or to discard a render entirely), so impure renders are bugs.
- Effects mount → unmount → mount again.
useEffect(anduseLayoutEffect) fire, then React simulates an unmount by running the cleanup, then fires the effect again. So auseEffect(() => { subscribe(); }, [])without a cleanup leaks one subscription on every dev mount. Two subscriptions go to one socket, double events fire, requests duplicate. This is the same shape of bug that would later appear in production when React's Offscreen / activity APIs unmount and remount a tree to keep state alive in the background. StrictMode forces you to write idempotent setups paired with symmetric teardowns.
- Reducers and
useStatelazy initializers run twice. If your reducer oruseState(() => initialize())initializer is impure (logs, fetches, allocates resources), you'll see it. Reducers must be pure; initializers should be too.
- Refs and
useMemofactory functions may be invoked more than once. Don't store "do this once" side effects inuseMemo— that's an anti-pattern StrictMode exposes.
- Deprecation warnings for unsafe legacy lifecycles (
componentWillMount,componentWillUpdate,componentWillReceiveProps), string refs, findDOMNode, and old context API.
Why React designed it this way. Concurrent rendering can pause an in-flight render, drop it for a higher priority update, then redo it. With Suspense and Offscreen, a subtree can be mounted invisibly, frozen, then re-mounted later. Hot reload re-mounts components without a navigation. If your component assumes "useEffect runs exactly once," any of these patterns will silently break in prod with cache duplication, event handler double-fire, or stale data. By double-invoking in dev, StrictMode collapses the timeline so the bug shows up on first reload, where you can fix it.
The mental model shift: stop thinking "useEffect runs on mount." Think "useEffect runs whenever React decides the component is being attached, possibly more than once. Whatever I do in the setup, I must reverse in the cleanup so any number of attach/detach cycles is correct."
Common fixes:
useEffect(() => { const id = bus.subscribe(h); return () => bus.unsubscribe(id); }, [])— pair every subscribe with an unsubscribe.- For fetches, use an
AbortControllerwhoseabort()is the cleanup, plus an "ignore" flag if you setState in the response. - For event listeners and timers, always return a cleanup that mirrors setup.
- For one-time module-level work (analytics init), guard with an
if (initialized) return;outside the effect, or move to a ref.
Production behavior. None of the double-invocation runs in production builds, so don't write code that depends on it. But the bugs it exposes are real — they manifest in production via concurrent features, hot reload, or future Offscreen support. Keeping StrictMode on in dev is one of the cheapest investments in app correctness.
Code
Follow-up questions
- •Why is double-invocation only in development?
- •How does this interact with data fetching in effects?
- •What's the right pattern for one-time setup?
Common mistakes
- •Disabling StrictMode to silence the issue instead of fixing the effect.
- •Initializing analytics / network calls in effects without idempotency.
Performance considerations
- •Dev-only — zero production impact.
- •Surfaces bugs that would otherwise cause double-fetches and stale subscriptions.
Edge cases
- •Refs created in render are stable across the double mount, but effects that *use* refs may still need defensive resets.
- •Animations started in effects need to be cancelled on cleanup or you'll see two playing.
Real-world examples
- •Auto-saving an analytics 'page_view' in useEffect → fires twice in dev, twice in prod transitions. Move to a route-change handler instead.