DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Why Your useEffect Runs Twice in Development (And Why You Shouldn't "Fix" It)

This causes a very specific, very common reaction, a developer sees a console.log inside useEffect fire twice in the browser console during local development, assumes something's genuinely broken, and starts debugging a bug that doesn't actually exist.

What's Actually Happening

'use client';
import { useEffect } from 'react';

export function Tracker() {
  useEffect(() => {
    console.log('Effect ran'); // logs twice in development, once in production
    trackPageView();
  }, []);

  return null;
}
Enter fullscreen mode Exit fullscreen mode

In development, with React's Strict Mode enabled, which the Next.js App Router enables by default, React deliberately mounts a component, runs its effects, unmounts it, and immediately remounts it again, running the effects a second time. This is not a bug, and it's not accidental, it's a specific, intentional development-only behavior designed to help surface exactly the kind of bug this pattern causes in the first place, effects with real, unintended side effects or missing cleanup.

Why React Does This on Purpose

React is preparing for a future where components can be safely paused, discarded, and remounted, for features like offline support and certain rendering optimizations, and that capability genuinely requires effects to be safely re-runnable, mount, unmount, mount again, without producing broken or duplicated behavior. The double-invoke in development is a deliberate stress test, if your effect breaks when run twice in a row, mount, unmount, mount, it's revealing a genuine correctness gap, code that assumes it only ever runs once, which was never actually a safe assumption to begin with, even before Strict Mode made it visible.

Why the Instinct to "Fix" This Is Usually Wrong

Seeing double logs or double network calls in development and reaching for a way to suppress it, disabling Strict Mode entirely, or adding a fragile useRef guard specifically to prevent the second invocation, treats the symptom as the problem. The actual problem, if there is one, is that the effect wasn't written to safely handle being cleaned up and re-run, which is a real gap worth fixing properly, not hiding by preventing Strict Mode from ever revealing it again.

// ❌ A common "fix" that just hides the signal instead of addressing what it revealed
const hasRun = useRef(false);

useEffect(() => {
  if (hasRun.current) return;
  hasRun.current = true;
  trackPageView();
}, []);
Enter fullscreen mode Exit fullscreen mode

This pattern specifically defeats the entire point of Strict Mode's double-invoke check, and worse, it can behave inconsistently across React versions and concurrent rendering scenarios in ways that are genuinely harder to reason about than just writing the effect correctly in the first place.

The Actual Fix: Write Effects That Are Safe to Run Twice

For something like an analytics call, the real question worth asking is whether firing it twice in development actually matters, and for most analytics providers, it doesn't, in production, without Strict Mode's double-invoke, it only fires once anyway. The development-only double log is not the same as double-counting real production analytics data, a distinction worth confirming directly rather than assuming.

useEffect(() => {
  // In production, this fires once. In development, Strict Mode fires it
  // twice, which is expected and does not reflect production behavior.
  trackPageView();
}, []);
Enter fullscreen mode Exit fullscreen mode

For an effect that sets up something genuinely stateful, a subscription, an interval, an event listener, the actual fix is a real, correct cleanup function, which is precisely what Strict Mode's double-invoke is designed to verify you've written:

useEffect(() => {
  const interval = setInterval(() => {
    checkForUpdates();
  }, 5000);

  return () => clearInterval(interval); // correctly cleans up, safe to run twice
}, []);
Enter fullscreen mode Exit fullscreen mode

With a correct cleanup function, the double-invoke in development runs cleanly, set up, tear down, set up again, exactly the pattern Strict Mode exists to confirm your code actually handles safely. If removing the cleanup function causes something to visibly break or duplicate in development, that's Strict Mode doing its job, not React misbehaving.

Where This Actually Matters for Real Bugs, Not Just Console Noise

This connects directly to the stale fetch race condition covered in an earlier post, an effect fetching data without a cleanup function that ignores stale responses is exactly the kind of effect that also breaks under Strict Mode's double-invoke in development, showing a flash of duplicated or incorrect state before settling, which is a genuine, useful early warning for a bug that would otherwise only surface in production under real network timing variance.

The Actual Rule

A useEffect that produces visibly different or broken behavior when Strict Mode runs it twice in development almost always has a real gap, missing or incorrect cleanup, not properly guarding against being re-invoked, that would eventually cause a genuine bug in production too, just under different, harder-to-reproduce conditions. The fix is writing the effect to genuinely handle mount, unmount, remount correctly, not suppressing the specific mechanism designed to reveal that it doesn't.

I keep Strict Mode enabled across every project I build, client work and the templates at pixelanas.com alike, specifically because catching this category of bug in development, as annoying as the double logs are at first, is far cheaper than debugging the same underlying issue once it surfaces as an intermittent, hard-to-reproduce production bug instead.


If you've disabled Strict Mode, or added a ref-based guard specifically to suppress double-invoked effects, worth reconsidering whether that's hiding a real, fixable gap rather than actually solving anything. Drop your own experience with this in the comments.

Get the templates: https://pixelanas.gumroad.com


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (0)