Strict Mode and Double-Invoked Effects
Understand what Strict Mode actually does in development, why React intentionally double-invokes component render functions and effect setup/cleanup pairs, and how to write effects that are resilient to running twice.
Strict Mode doesn't render anything visible and has zero effect on your production build's behavior -- it's a development-only tool that intentionally runs certain code twice in order to surface bugs that would otherwise stay hidden until they caused real problems for users, particularly around effects that aren't properly cleaned up.
What Strict Mode actually does
Strict Mode is enabled by wrapping part (or all) of your tree in <StrictMode>. It performs a set of extra development-only checks: it renders components an extra time to help surface side effects hidden in the render body, it re-runs effect setup and cleanup functions an extra time on mount to check that cleanup is correctly implemented, and it warns about usage of deprecated or legacy APIs. None of these checks run in a production build -- production builds render and mount exactly once per actual trigger, exactly as you'd expect.
The entire point of Strict Mode is to convert bugs that would otherwise only appear intermittently in production -- under specific timing, on slow networks, or after a future React feature ships -- into bugs that reproduce reliably and immediately in development, while you're actively working on the code and best positioned to fix them.
Strict Mode can be applied to any subtree, not just the whole app -- wrapping just a component you're actively developing lets you get its checks without forcing double-render/double-effect behavior across code you haven't gotten to yet.
Why effects are double-invoked on mount in development
Specifically for effects, Strict Mode in development runs the sequence setup -> cleanup -> setup again, immediately, the first time a component with an effect mounts. This is not a bug and not something to silence -- it deliberately simulates what already happens for real in a component's lifetime: a component can mount, unmount, and remount (for example, when it's inside a route that navigates away and back, inside a list item that gets removed and re-added, or under future React features that intentionally remount offscreen content to preserve state elsewhere).
If your effect's setup function starts something (a subscription, a timer, an event listener, an open connection) and its cleanup function properly tears that same thing down, then running setup -> cleanup -> setup is harmless and produces the exact same end state as running setup once -- you simply end up subscribed once, with no leaks and no duplicate side effects. It's only effects with missing or incorrect cleanup that misbehave under this double-invocation, and that misbehavior is exactly the bug Strict Mode is designed to expose early.
This behavior applies to the effect's setup and cleanup functions specifically, in development, on mount -- it is not React silently running your effect twice in production, and it is not evidence that React itself is unreliable; it is a deliberate stress test of a guarantee your code is supposed to uphold regardless of whether Strict Mode is present.
Detecting and fixing unsafe patterns
The most common failure mode Strict Mode surfaces is an effect that creates something in its setup function but has no cleanup function at all, or a cleanup function that doesn't fully undo what setup did. A classic case: opening a WebSocket connection in setup with no corresponding socket.close() in cleanup -- under double-invocation, this leaves two live connections open instead of one, which is easy to spot in development (extra network activity, duplicate incoming messages) precisely because Strict Mode forced it to happen immediately rather than only under rare real-world remounts.
Another common pattern is an effect that mutates a variable outside of React's state system as if it's guaranteed to run exactly once -- for example, incrementing a plain module-level counter inside an effect body to 'count how many times this component has mounted.' Under Strict Mode's double-invocation, that counter is off by design, which is the mechanism working as intended, not a defect to work around with an if-statement that skips the second run.
The fix is essentially always the same: make sure the cleanup function undoes everything the setup function did, so the effect is safe to run any number of times in a row without accumulating state. Writing effects this way isn't just about satisfying Strict Mode -- it's the same property that keeps an app correct when a component legitimately mounts and unmounts repeatedly during normal use, so treating 'my effect must survive setup->cleanup->setup' as a real production requirement, not a development-only annoyance, is the right mental model.
Wrapping a subtree in Strict Mode
StrictMode adds no visible UI -- it only changes development-time behavior (extra renders, extra effect setup/cleanup cycles, extra warnings). It has no effect at all in a production build. It can wrap the whole app or just a subtree you're actively developing.
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);Code Examples
With no cleanup function returned, Strict Mode's development-only double-invocation (setup, then simulated cleanup that does nothing, then setup again) actually opens a second real connection, since there was nothing to tear down the first one. This exposes a real leak that would also occur any time this component legitimately remounts in production.
Common mistake: Treating the extra connection seen only in development as a Strict Mode quirk to ignore, rather than a real missing-cleanup bug.
Now Strict Mode's setup -> cleanup -> setup sequence runs: connect, disconnect, connect -- ending in exactly one open connection, identical to what a single setup call would have produced. The effect is resilient to being invoked twice because cleanup genuinely reverses setup. Expected result: Development: two connect/disconnect cycles logged, ending with exactly one active connection. Production: one connect call, one active connection.
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const socket = createSocket(roomId);
socket.connect();
// BUG: no cleanup -- under StrictMode's setup->cleanup->setup,
// this leaves TWO open connections instead of one.
}, [roomId]);
return <div>Connected to {roomId}</div>;
}function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const socket = createSocket(roomId);
socket.connect();
return () => {
socket.disconnect(); // cleanup fully undoes setup
};
}, [roomId]);
return <div>Connected to {roomId}</div>;
}Strict Mode's development-only mount sequence for effects
The double-invocation is a stress test: if your effect is idempotent under setup->cleanup->setup, Strict Mode changes nothing observable about the end result.
Normal (production) mount:
setup()
Strict Mode development mount:
setup()
cleanup() <-- immediately, simulating an unmount
setup() <-- immediately, simulating a remount
Final state after either path should be identical
IF cleanup() fully undoes what setup() did.Important Rules
Strict Mode is a development-only tool -- it renders nothing and has no effect on production behavior.
In development, Strict Mode intentionally runs setup -> cleanup -> setup for each effect once on mount, to surface missing or incorrect cleanup.
An effect is correctly written when running its setup and cleanup any number of times in a row leaves the app in the same state as running setup once.
Do not write code that works around double-invocation with flags or guards that skip the second run -- fix the missing cleanup instead.
Strict Mode can wrap the whole app or a single subtree, letting you opt specific components into its checks incrementally.
Where this shows up
A component that subscribes to a WebSocket, a browser event listener, or a third-party SDK instance inside useEffect is exactly the pattern Strict Mode's double-invocation is designed to test -- catching a missing socket.close() or removeEventListener in development, during a quick local test, is far cheaper than discovering duplicate live connections or duplicate event handling only after users start reporting odd behavior in production.
let hasRun = false; useEffect(() => { if (hasRun) return; // "fixing" the double call by skipping it hasRun = true; startAnalyticsSession(); }, []);
useEffect(() => { const sessionId = startAnalyticsSession(); return () => endAnalyticsSession(sessionId); }, []);
Suppressing the second invocation with a guard flag hides the real problem (missing cleanup) instead of fixing it, and leaves the component just as broken the next time it genuinely mounts, unmounts, and remounts in production outside of Strict Mode.
Does Strict Mode's double-rendering and double-invoking of effects happen in production builds? A. Yes, always, in every environment B. No -- it is a development-only behavior with no effect on production C. Only if the user's browser is in an unusual state D. Only for class components, never for function components
B. No -- it is a development-only behavior with no effect on production — Strict Mode's extra renders and its setup->cleanup->setup effect sequence are development-only checks meant to surface bugs early; production builds run everything exactly once per real trigger.
Why does React deliberately run setup -> cleanup -> setup for effects on mount in Strict Mode? A. To make development builds run faster B. To simulate a component mounting, unmounting, and remounting, exposing effects whose cleanup is missing or incomplete C. Because effects are always run twice in production too, so this just previews that D. To test network requests specifically, and nothing else
B. To simulate a component mounting, unmounting, and remounting, exposing effects whose cleanup is missing or incomplete — Real components can legitimately unmount and remount during normal use. Strict Mode forces that sequence to happen immediately and reliably in development, so an effect that doesn't clean up after itself properly reveals the bug right away instead of only under rare, hard-to-reproduce real remounts.
An effect opens a WebSocket in its setup function and returns no cleanup function. What happens the first time this component mounts under Strict Mode in development? A. Exactly one connection opens, same as production B. Two separate connections end up open, because there was no cleanup to close the first one before setup ran again C. React throws a runtime error immediately D. The component fails to render at all
B. Two separate connections end up open, because there was no cleanup to close the first one before setup ran again — With no cleanup function, the simulated cleanup step does nothing, so the socket from the first setup call is still open when the second setup call runs and opens another one -- exposing the missing-cleanup bug that would otherwise only manifest on a real remount in production.
A component's effect starts a setInterval timer that pushes a value into an array every second, and has no cleanup function. Under Strict Mode in development, describe what you would observe, and write the corrected effect.
Because cleanup does nothing, the simulated unmount doesn't actually clear the first interval, so the second setup call starts a second, independent interval running alongside the first -- you'd observe values being pushed roughly twice as often as expected, or duplicated pairs of pushes. The fix is: useEffect(() => { const id = setInterval(() => pushValue(), 1000); return () => clearInterval(id); }, []); -- now cleanup actually clears the timer, so setup->cleanup->setup ends with exactly one running interval, matching a single setup call.
Think about how many intervals end up running simultaneously after setup->cleanup->setup with no real cleanup.