React useEffect Firing Twice in Development: What's Happening and How to Fix It

June 20, 2026 9 min read 2 views

You wire up a useEffect to fetch data or set up a WebSocket, refresh the page, and watch your network tab show two identical requests. You add a console.log inside the effect and it prints twice. Nothing in your code calls the component twice, yet here you are. This is not a bug β€” it is React doing exactly what it was designed to do in development mode, and understanding it will make you a much better React developer.

What You'll Learn

  • Why React fires useEffect twice in development and not in production
  • The exact mount-unmount-mount lifecycle sequence Strict Mode creates
  • How to write proper cleanup functions that handle the double-fire gracefully
  • How to deal with effects that genuinely cannot run twice (analytics, one-time API calls)
  • Common mistakes that turn a minor annoyance into real bugs

What's Actually Happening

In React 18, wrapping your app in <React.StrictMode> causes every component to mount, unmount, and mount again β€” all silently, in development only. This remounting also triggers every useEffect twice. You will not see the double-fire in a production build because Strict Mode is stripped out entirely at that point.

If you bootstrapped your project with Create React App or Vite's React template, Strict Mode is almost certainly already enabled. Open your main.jsx (or index.js) and you'll see something like this:

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);

That <StrictMode> wrapper is your culprit. It's also your friend, once you understand what it's telling you.

Why React 18 Introduced This Behavior

React's concurrent rendering model allows the framework to pause, discard, and replay renders at will. In the future, React may mount and unmount components multiple times as part of features like Offscreen rendering β€” where a component is pre-rendered in the background and then revealed instantly. Strict Mode's double-invoke behavior is a rehearsal for that reality.

The core message React is sending you: your effects must be resilient to running more than once. If your effect breaks when called a second time, it will also break in future React versions under real concurrent conditions β€” not just in development. Strict Mode is surfacing the problem early so you can fix it now rather than in production.

The Lifecycle Sequence You Need to Understand

In development with Strict Mode, the sequence for a component with a useEffect looks like this:

  1. Component mounts β†’ effect runs
  2. React immediately unmounts the component β†’ cleanup function runs (if you provided one)
  3. React remounts the component β†’ effect runs again

In production, steps 2 and 3 never happen. You get mount β†’ effect, full stop. This is why bugs caused by the double-fire are invisible in production and only surface in development β€” which is exactly the point.

Here is a minimal example that makes this sequence visible:

import { useEffect } from 'react';

function Timer() {
  useEffect(() => {
    console.log('Effect ran');

    return () => {
      console.log('Cleanup ran');
    };
  }, []);

  return <p>Timer component</p>;
}

In development you'll see: Effect ran β†’ Cleanup ran β†’ Effect ran. In production you'll only see: Effect ran.

Effects That Break When Called Twice

Not every effect is equally affected. Simple state updates and DOM reads are usually fine. The effects that genuinely break fall into a few categories:

  • WebSocket or SSE connections β€” opening a connection twice without closing the first one leaks a connection.
  • Interval and timeout IDs β€” calling setInterval twice stacks two intervals, each ticking independently.
  • Event listeners added to the DOM or window β€” you end up with duplicate listeners firing once for every registered copy.
  • Non-idempotent API calls β€” a POST that creates a record in a database will create two records if the effect fires twice.
  • Third-party library initializations β€” many libraries (maps, charts, rich-text editors) throw an error or render incorrectly if you call their init function twice on the same container.

Writing Cleanup Functions That Actually Work

The correct fix for the vast majority of double-fire problems is a proper cleanup function. React runs the cleanup before re-running the effect, so a well-written cleanup function makes the double-fire completely harmless.

Intervals and Timeouts

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);

  return () => clearInterval(id);
}, []);

The cleanup clears the first interval before the second one is created. In production there is only one interval. In development there are briefly two, but the first is cleared immediately, so the result is the same: exactly one running interval when the component is live.

WebSocket Connections

useEffect(() => {
  const socket = new WebSocket('wss://example.com/feed');

  socket.onmessage = (event) => {
    setData(JSON.parse(event.data));
  };

  return () => {
    socket.close();
  };
}, []);

Strict Mode will open a connection, close it, then open a fresh one. Your server will see two quick handshakes during development, but the component will only hold one active connection at any given time. That is the correct behavior.

Fetch Requests with AbortController

Fetch is where developers most often feel the pain. You don't want two network requests firing every time a component mounts. Use an AbortController to cancel the in-flight request when the cleanup runs:

useEffect(() => {
  const controller = new AbortController();

  async function fetchUser() {
    try {
      const res = await fetch('/api/user', { signal: controller.signal });
      const data = await res.json();
      setUser(data);
    } catch (err) {
      if (err.name !== 'AbortError') {
        setError(err.message);
      }
    }
  }

  fetchUser();

  return () => controller.abort();
}, []);

The first fetch is aborted immediately when the cleanup runs. The second fetch completes normally. You only ever set state from one resolved request. If you are building a larger React application that pairs a Django backend, this pattern also keeps your Django REST Framework endpoints from receiving unnecessary duplicate calls during development.

DOM Event Listeners

useEffect(() => {
  function handleResize() {
    setWidth(window.innerWidth);
  }

  window.addEventListener('resize', handleResize);

  return () => window.removeEventListener('resize', handleResize);
}, []);

This is the most straightforward case. removeEventListener must receive the exact same function reference that was passed to addEventListener β€” which is why the function is defined inside the effect and referenced in both calls.

When You Cannot Add a Cleanup Function

Some effects genuinely have no inverse operation. You cannot un-send an email or un-create a payment intent. For truly one-shot side effects, the idiomatic React approach is to move that logic out of useEffect entirely and trigger it from a user action (a button click, a form submit) rather than from a mount event.

If you absolutely must fire a one-shot effect on mount β€” say you're integrating a third-party widget that crashes if initialized twice β€” you can use a ref as a guard flag:

import { useEffect, useRef } from 'react';

function MapWidget() {
  const initialized = useRef(false);

  useEffect(() => {
    if (initialized.current) return;
    initialized.current = true;

    // Safe to call once
    ThirdPartyMap.init('#map-container');
  }, []);

  return <div id="map-container" />;
}

Use this pattern sparingly. It suppresses React's double-invoke check, meaning any bugs that the check would have caught will now silently survive into your codebase. Treat it as a last resort for third-party integrations, not a general escape hatch.

A cleaner long-term solution is to abstract initialization into a side-effect-aware singleton or a custom hook that tracks initialization state internally, away from the component lifecycle.

Logging and Analytics: A Special Case

Analytics calls β€” page views, impression events, conversion tracking β€” are uniquely painful because they are often genuinely non-idempotent and the double-fire inflates your development metrics. There is no cleanup that "un-fires" an analytics event.

The practical answers here are:

  • Accept it in development. Development data is almost always excluded from analytics dashboards via hostname filtering. Double events in localhost usually do not matter.
  • Gate on an environment variable. Only send the event when process.env.NODE_ENV === 'production'. This is a legitimate use case, not a hack.
  • Move the call outside React. If an analytics event needs to fire exactly once on page load, fire it in a plain script tag before React mounts rather than inside a component's effect.
useEffect(() => {
  if (process.env.NODE_ENV !== 'production') return;
  analytics.track('page_view', { page: '/dashboard' });
}, []);

This is a pragmatic trade-off. You are not fixing the underlying issue, but you are containing the damage to a known, controllable scope.

Common Mistakes Developers Make

Understanding the theory is one thing; here are the mistakes that actually trip people up in real projects.

Removing Strict Mode to "Fix" the Double-Fire

This is the most common knee-jerk reaction and the worst response. Disabling Strict Mode silences the symptom and leaves the underlying fragility in your codebase. If React ever remounts your component for a legitimate reason β€” like a future Offscreen API β€” your broken effect will surface in production. Keep Strict Mode on and fix the effect properly.

Using a State Variable as a Guard Instead of a Ref

Developers sometimes try to guard against double-fires with a boolean state variable. This does not work reliably because state updates trigger re-renders, which can create new race conditions. A useRef holds a mutable value that does not cause re-renders, making it the correct tool when you genuinely need an initialization flag.

Forgetting That Cleanup Runs Before Re-run, Not After

React runs the cleanup from the previous effect invocation before running the effect again. This catches many developers off guard. When you see the sequence cleanup β†’ effect, it does not mean the component unmounted and remounted in the traditional sense. It means React is calling your cleanup and then your effect in the same synchronous tick, as part of its development-mode verification step. This is similar to how understanding async execution order in other runtimes requires you to shift your mental model slightly before things click into place.

Not Handling AbortError Separately

When you abort a fetch, the promise rejects with an AbortError. If your catch block calls setError unconditionally, you'll set an error state on every clean development mount because the first fetch is always aborted. Always check err.name !== 'AbortError' before treating a fetch failure as a real error.

Stacking Multiple Effects Without Cleaning Each One

It's tempting to put everything into a single useEffect. When that effect contains multiple subscriptions or listeners, a missing cleanup for any one of them leaks that resource. Split effects by concern and give each one its own cleanup. This is also easier to debug when something behaves unexpectedly β€” you can identify which effect is misbehaving by process of elimination.

Wrapping Up

The double-fire is React telling you something useful: your effect is not resilient enough to run more than once. Rather than treating it as noise, treat it as a failing test. Here are the concrete steps to take right now:

  • Audit every useEffect in your project and confirm each one has a cleanup function that properly tears down whatever the effect set up.
  • Replace raw fetch calls inside effects with the AbortController pattern and handle AbortError explicitly in your catch block.
  • Replace bare setInterval and setTimeout calls with versions that return the ID to the cleanup function via clearInterval / clearTimeout.
  • Move one-shot non-idempotent operations (form submissions, payment triggers, analytics) out of effects and into event handlers wherever possible.
  • Leave Strict Mode enabled and use the double-fire as your canary: if an effect breaks when called twice in development, it will break in production eventually.

Frequently Asked Questions

Does useEffect firing twice in development mean there's a bug in my React code?

Not necessarily. React 18 intentionally fires useEffect twice in development when Strict Mode is enabled, to help you catch effects that are not resilient to re-running. If your effect works correctly after writing a proper cleanup function, there is no bug.

Will useEffect fire twice in production builds?

No. The double-fire behavior is exclusive to development mode and only occurs when your app is wrapped in React.StrictMode. Production builds do not include Strict Mode, so effects run exactly once per mount in production.

Is it safe to remove React.StrictMode to stop useEffect from firing twice?

Technically it works, but it is a bad idea. Strict Mode exists to surface fragile effects before they cause production bugs. Removing it hides the problem rather than fixing it, and future React concurrent features may remount components legitimately, exposing the issue in production.

How do I stop a fetch request from running twice inside useEffect?

Use an AbortController and return its abort method as the cleanup function. React will abort the first fetch when it runs the cleanup, and the second fetch will complete normally. Make sure to catch AbortError separately so you do not treat the cancelled request as a real error.

Why does my analytics event get tracked twice when the component mounts?

Strict Mode's double-invoke behavior causes the effect to fire twice in development, sending two analytics events. The cleanest fix is to gate the call with a NODE_ENV check so it only runs in production, or to move the tracking call into a user-triggered event handler rather than a mount effect.

πŸ“€ Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

πŸ“¬ Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.