Modern Web · React

React 19 Strict Mode Patterns

What React 19's Strict Mode actually double-invokes in development — render, effects, and now state initializers — why that catches missing cleanup functions, and how to debug the bugs it surfaces instead of fighting it.

John Kihiu12 min read

Strict Mode has a reputation for "breaking" apps that seemed fine a minute earlier, and almost every time I've debugged one of those reports, the bug was already there — Strict Mode just made it visible by running things twice. It doesn't change behavior in production; it exists purely to surface impure code in development before it becomes a production incident.

What actually gets double-invoked

Wrapping a subtree in <StrictMode> makes React, in development only, call component function bodies twice per render, run effect setup-then-cleanup-then-setup again on mount, and — newer in the React 19 line — call state initializer functions (the function form passed to useState, and reducer init functions) twice as well. Production builds are unaffected; none of this doubling ships to users.

TSX · a state initializer that isn't pure
let callCount = 0;

function ExpensiveList() {
  // Strict Mode calls this initializer twice in dev — if it has a
  // side effect, that side effect now happens twice.
  const [items] = useState(() => {
    callCount++; // impure: a side effect inside what should be pure setup
    return buildInitialItems();
  });
  return 
    {items.map((i) =>
  • {i.label}
  • )}
; }

That callCount increment is exactly the kind of bug Strict Mode is designed to catch: a state initializer is supposed to be a pure computation of the initial value, and if it has a side effect, doubling it in development turns a subtle production bug (the side effect running an extra time under some future React optimization) into an obvious one you'll notice immediately.

Why double-invoking effects catches missing cleanup

The effects case is the most common one people hit. React runs mount → cleanup → mount for every effect in development, which means any effect that opens a subscription, timer, or connection without returning a cleanup function will now visibly leak — you'll see two active subscriptions, a duplicated event listener, or a timer firing twice, instead of one silent copy that only causes a slow memory leak in production.

TSX · missing cleanup, exposed by strict mode
useEffect(() => {
  const id = setInterval(() => tick(), 1000);
  // no return statement — the interval is never cleared.
  // Strict Mode's mount→cleanup→mount cycle means you now get
  // two intervals ticking, which is obvious in a way one leaked
  // interval in production never was.
}, []);

// fixed:
useEffect(() => {
  const id = setInterval(() => tick(), 1000);
  return () => clearInterval(id);
}, []);

Interaction with Actions and use()

Actions and use() don't introduce new Strict Mode behavior on their own, but they raise the stakes on the same rule: the function you pass to an Action or the promise you pass to use() needs to tolerate being invoked as part of a double-render, since the component calling them is still subject to the same double-invocation in development. An Action's async body running twice in dev because its parent re-rendered twice is expected; an Action with a side effect that assumes it only ever runs once (like an in-memory counter, or a ref mutated unconditionally during render) will misbehave the same way an impure state initializer does.

Strict Mode isn't testing your effects twice for fun

Every double-invocation exists to answer one question: "if React scheduled this twice, would anything break?" Because concurrent features may re-run render or commit work, that question matters in production even though the doubling itself is dev-only.

Debugging advice: it was already broken

When Strict Mode surfaces a failure, resist the urge to remove <StrictMode> to make the error go away — that hides the bug rather than fixing it. The usual culprits are effects relying on running "exactly once" (analytics pings, a one-time redirect, an animation that assumes a single mount), and refs or module-level variables mutated directly during render instead of in an effect. In both cases the fix is the same: make the operation idempotent or guard it, rather than fighting the double-invocation.

A ref mutated during render is a red flag, strict mode or not

If renderCountRef.current++ sits directly in a component body (not inside an effect or event handler), Strict Mode's double render will make the count look wrong — but the real problem is mutating a ref during render at all, which was never safe even before Strict Mode's behavior changed.

Wrapping up

Strict Mode doesn't add bugs; it removes the ability of certain bugs to hide. Double-invoking render, effects, and now state initializers all serve the same purpose: catching impure code in development, cheaply, before a production incident does it for you at a much worse time. If enabling it breaks something, the correct response is almost always to fix the underlying impurity, not to remove the wrapper that found it.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.