Strict Mode in React 18: What It Breaks and Why That's Good

React 18's strict mode double-invokes effects by design. Most developers disable it when it starts breaking things. That's the wrong response — the break is a signal, not a false alarm. Here's what strict mode actually does and why every bug it surfaces is a real bug.
At some point in a React 18 project, you've probably seen this: an effect that runs twice, a subscription that stacks, a timer that fires double, an animation that plays twice on mount. You check the code. It looks fine. You check the docs. You find that React 18 strict mode intentionally double-invokes effects in development.
The common response: add // eslint-disable or switch to useLayoutEffect or, in the most drastic cases, remove <StrictMode> from the app entirely.
All of these are wrong responses. The double invocation isn't a quirk to work around — it's a detector for a real category of bugs. If your effect breaks when React mounts and unmounts it once, it will break in production too, just less predictably.
What strict mode does in React 18
<StrictMode> is a development-only tool. It has no effect in production builds.
In React 18 development mode, strict mode does three things:
1. Double-invokes render functions. Your component renders twice. The second render is thrown away — but if your render has side effects (it shouldn't), this surfaces them.
2. Double-invokes state initializers and reducers. The function passed to useState and useReducer runs twice. If your initializer has side effects, this surfaces them.
3. Mounts, unmounts, and remounts every component once. This is the new behaviour in React 18. Effects run, cleanup runs, effects run again. The final state is a component that has been mounted twice and cleaned up once — as if the component appeared, disappeared, and appeared again.
The third behaviour is the one that causes the visible breakage.
Why React does this
React 18 introduced concurrent features — useTransition, useDeferredValue, and the upcoming ability to serialise and restore component state for performance. In concurrent React, the runtime can mount a component, unmount it, and remount it later to restore state. This might happen when:
- A component is offscreen but might be shown again
- React needs to pause and discard work to handle a more urgent update
- Future features like Activity (the API previously called Offscreen) restore cached component trees
Strict mode's double-mount simulates this behaviour in development so you find the bugs before they appear in production under concurrent rendering.
The guarantee React is asking you to uphold: effects must be idempotent across mount → unmount → remount cycles. The cleanup function must fully reverse what the setup function did. If it doesn't, the second mount leaves the component in a broken state.
The bugs strict mode surfaces
Subscriptions that stack
In production, single-mount React: one listener added, works fine. In strict mode: listener added, component unmounts (cleanup runs — but there's no cleanup), component mounts again, second listener added. Now every app:notification event triggers the handler twice.
Fix: always return a cleanup function that removes what you added.
Strict mode's double invocation runs setup, cleanup, setup. You end up with exactly one listener — the same state as a single mount. The effect is idempotent.
Timers that double-fire
In strict mode: interval starts, unmount (no cleanup — interval still running), remount, second interval starts. Now setSeconds fires twice per second. State increments at double speed.
Connections that open twice
In strict mode: connection opens, unmount (no cleanup — connection still open), remount, second connection opens. Now you have two WebSocket connections. Messages arrive twice. Events fire twice.
The effects that appear to break but are actually fine
Some effects look broken under strict mode but aren't — they just look different.
Fetch requests that fire twice
In strict mode: fetch fires, response arrives (or is abandoned), cleanup runs (nothing to clean up), fetch fires again. You see two network requests in the dev tools.
This isn't breaking your app. The second fetch replaces the first. The data is correct. The appearance of two requests is informational — it's showing you that your fetch has no cancellation mechanism, which in production could cause a race condition if the component unmounts before the response arrives.
The right response is not to remove strict mode — it's to add cancellation:
Now strict mode's double-invocation: fetch starts, cleanup aborts it, fetch starts again. One request in flight. The race condition Alex described in Article #1 is handled.
Analytics events that fire twice
This one is genuinely awkward. You don't want two page view events in development. The right solution: either accept the duplication in development and filter it in your analytics dashboard, or wrap the call in a ref to ensure it only fires once:
Or use a useRef that persists across the double-mount:
Reading strict mode output in React DevTools
React DevTools 4.18+ shows which renders are from strict mode's double-invocation. In the Profiler, strict mode renders are dimmed — you can distinguish "real" renders from strict mode validation renders.
In the Components tab, components mounted under strict mode show a small indicator. When debugging, this helps you distinguish "this rendered because state changed" from "this rendered because strict mode is checking it."
The three wrong responses
Disabling strict mode. The most common mistake. The bugs that strict mode surfaces will appear in production under concurrent React features. You've silenced the detector without fixing the problem.
Switching to useLayoutEffect to avoid double-invocation. useLayoutEffect is also double-invoked in strict mode. This doesn't solve anything.
Adding a useRef guard to prevent the second setup from running. This defeats the purpose of the check. If your effect can't run twice safely, it can't run under concurrent remounting safely — and you've just hidden the bug.
The one right response
Fix the cleanup function. Every setup has a corresponding teardown. That's the contract.
If you can't write a cleanup function — if the operation is truly one-way and irreversible — that operation probably belongs in an event handler, not a useEffect. Effects are for synchronisation. Event handlers are for one-time imperatives.
What to do when you inherit a codebase with strict mode removed
Put it back. Run the app in development. Fix every effect that breaks. It will surface real bugs — subscriptions that leak, timers that stack, connections that accumulate. Each one is a bug you'd rather find in development than in a bug report.
The time investment is front-loaded. Once effects are written correctly, strict mode is silent. It only makes noise when there's something worth making noise about.
Up next: How to build a component library with proper versioning — the final article in the React Foundations series, covering the step from building components to distributing them.
Related: useEffect, What Is It Really? by Muhammad Athar — the companion explainer that covers cleanup functions in depth, which is the fix for every strict mode violation.
Alex Chen is a senior frontend engineer who writes about React patterns, JavaScript internals, and the decisions that separate maintainable codebases from ones that fight back. Opinionated by design.
Photo by Kitty Hutchinson on Unsplash

