Strict Mode Double Effects
Why React Strict Mode double-invokes effects in development: finding missing cleanup, pure render, and what production does.
- react
- strict-mode
In development, React Strict Mode intentionally double-invokes certain logic so you notice missing cleanups and impure renders before production. Teams file bugs like “my effect fires twice and creates two WebSockets” — that is Strict Mode doing its job, not a production double-mount mystery.
Docs: Strict Mode, Synchronizing with Effects.
What gets double-invoked
On initial mount in development (React 18+):
- Component renders.
- Effects run (setup).
- Effect cleanups run immediately.
- Effects run setup again.
The product behavior you should design for: setup → cleanup → setup is always possible (navigate away and back, remount). Strict Mode just forces that path early.
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (e) => setMsg(e.data);
return () => {
ws.close(); // without this, Strict Mode shows two sockets
};
}, [url]);
What production does
Production does not double-invoke effects on mount. You get a single setup until deps change or unmount. Relying on “it only runs once” without cleanup still breaks on real remounts and Fast Refresh edge cases.
Pure render checks
Strict Mode also double-renders components in development to surface impure render bodies:
// Bad: mutating during render
function Bad({ items }) {
items.sort(); // mutates props; second render sees different order
return <List items={items} />;
}
// Good
function Good({ items }) {
const sorted = useMemo(
() => items.slice().sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
return <List items={sorted} />;
}
Not an excuse to disable Strict Mode
// Resist this “fix”
// remove <StrictMode> because analytics fired twice
Fix the effect: guard analytics with a server-side idempotency key, or fire from an event (“Purchase clicked”) instead of mount when that matches the product meaning. For connections and subscriptions, cleanup is mandatory either way.
Common false alarms
| Symptom | Real issue |
|---|---|
| Two network calls in dev | Missing abort/cleanup or OK if both aborted correctly |
| Counter increments by 2 in setState during render | Illegal setState in render / impure body |
| LocalStorage write twice | Write in effect without coordinating; or accept dual write in dev only |
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then((r) => r.json())
.then(setData)
.catch((err) => {
if (err.name !== 'AbortError') throw err;
});
return () => controller.abort();
}, [url]);
Interview out-loud
“Strict Mode in development remounts and re-runs effects to surface missing cleanups and impure renders. Production mounts once. I treat setup/cleanup as mandatory for subscriptions, timers, and fetches with AbortController, not as a dev-only annoyance.”
Related on this site
Further reading
Production checklist
Before you ship a change in this area, walk the list out loud:
- What is the source of truth for the data on screen?
- What happens on remount, route change, and Strict Mode double-invoke?
- Which updates are urgent (input) versus deferrable (filter large lists)?
- Did you profile before adding memo, context splits, or virtualization?
- Is there an accessibility path: keyboard, focus, names, and errors?
If you cannot answer those, the API knowledge will not save the interview or the incident.
Related guides
- Accessibility Patterns in ReactPractical React a11y: labels, focus management, keyboard, live regions, and composition patterns that stay accessible.
- Avoid Prop Drilling with CompositionStop threading props through intermediates: children slots, inversion of control, and when context is the right escape hatch.
- Batching State UpdatesHow React 18+ batches setState in events, timeouts, and promises: when updates flush and why double setState still works.
- Children Prop PatternsUsing children and slot props for flexible APIs: wrappers, compound components, and when to prefer explicit props.
- Client Component BoundariesWhere to put use client: push interactivity to leaves, serializable props, and children as server slots.