ESC

Type to search the knowledge base.

Strict Mode Double Effects

Why React Strict Mode double-invokes effects in development: finding missing cleanup, pure render, and what production does.

intermediate3 min read
  • 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+):

  1. Component renders.
  2. Effects run (setup).
  3. Effect cleanups run immediately.
  4. 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.”

Further reading

Production checklist

Before you ship a change in this area, walk the list out loud:

  1. What is the source of truth for the data on screen?
  2. What happens on remount, route change, and Strict Mode double-invoke?
  3. Which updates are urgent (input) versus deferrable (filter large lists)?
  4. Did you profile before adding memo, context splits, or virtualization?
  5. 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