🚀 Executive Summary

TL;DR: React components often render twice in development due to Strict Mode, which intentionally double-invokes `useEffect` to expose impure logic and missing cleanup functions. The core solution involves writing idempotent effects with proper cleanup to prevent unintended side effects like duplicate API calls, or discerning if the double execution is a non-issue for read-only operations.

🎯 Key Takeaways

  • React.StrictMode, active by default in development, intentionally runs `useEffect` setup and cleanup functions twice to help developers identify and fix impure side effects.
  • The professional fix for double-firing effects is to implement a cleanup function that makes the effect idempotent, often by using a flag (e.g., `ignore`) or `AbortController` to prevent obsolete operations from updating state.
  • Not all double renders are problematic; while `POST` requests or analytics events require immediate cleanup, simple `GET` requests for read-only data might be harmless in development and not require code changes.

What React mistakes caused you the most debugging time?

Confused by React components rendering twice in development? Learn why Strict Mode’s double invocation of useEffect is actually a feature, not a bug, and discover the right way to write idempotent effects with proper cleanup.

The Phantom Render: Why Your React Component is Firing Twice (and How to Fix It)

I remember it vividly. 2:17 AM. My phone buzzes with a PagerDuty alert. Metric spike on api-gateway-prod-04. Specifically, the /api/v1/user/:id/initiate-session endpoint was getting hit twice for every single new user login. We were doubling our session creation events, skewing our analytics, and triggering downstream workflows twice. A frantic search led us to a new onboarding component a junior dev had just shipped. The code looked innocent enough: a simple useEffect to fetch user data on mount. But there it was, the phantom call. The root cause wasn’t a bug in our backend or some weird network retry. It was a fundamental misunderstanding of how modern React works in development.

It’s Not a Bug, It’s a (Painful) Feature: Meet Strict Mode

If you’ve been banging your head against the wall wondering why your useEffect with an empty dependency array [] is running twice on component mount, you’re not alone. The culprit is almost always React.StrictMode.

Modern React frameworks like Create React App or Next.js wrap your entire application in a <React.StrictMode> component by default in your main entry file (like index.js). This is a developer-only tool. It doesn’t run in your production build. Its job is to be a drill sergeant for your components, intentionally running certain functions twice to help you find “impure” logic and side effects you forgot to clean up.

When Strict Mode is active, React will perform the following sequence for hooks like useEffect:

  1. Mount the component.
  2. Run the effect’s setup function.
  3. Immediately unmount the component.
  4. Run the effect’s cleanup function.
  5. Re-mount the component with the previous state.
  6. Run the effect’s setup function again.

So, that API call you put in your effect? It’s being set up, torn down, and set up again, leading to two network requests. React is doing this to force you to write proper cleanup logic, ensuring your component behaves predictably if it’s ever added, removed, and re-added to the DOM.

The Fixes: From Hacky to Professional

Okay, we know the “why.” Now, how do we stop the bleeding? Here are three ways to handle it, from the one I’d never approve in a PR to the one that shows senior-level thinking.

Solution 1: The “Turn It Off” Fix (Please Don’t)

The quickest, dirtiest way to stop the double-render is to find your index.js or main.jsx file and simply remove the <React.StrictMode> wrapper around your <App /> component.


// In your index.js or main.jsx

// BEFORE
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// AFTER
root.render(
  <App />
);

And just like that, the double render is gone. This is the equivalent of turning off a smoke detector because you burned some toast. You’ve silenced the alarm, but you’ve also disabled a critical safety tool that was trying to warn you about a potential fire. Don’t do this. You’re just hiding a potential bug that will bite you later.

Solution 2: The “Right Way” Fix – Embrace Cleanup & Idempotency

The professional solution is to listen to what React is telling you and write a proper cleanup function for your effect. The goal is to make your effect idempotent—meaning it can run multiple times but produce the same result as if it ran only once. For an API call, this means ensuring only one request actually completes and updates state.

Here’s a typical “buggy” effect that causes the double fetch:


// The Problematic Code
useEffect(() => {
  console.log("Effect is running...");
  fetchUserData(userId).then(data => {
    setUserData(data);
  });
}, [userId]);

When this runs twice, two fetchUserData calls are fired. We can fix this by adding a cleanup function that tells the first, now-obsolete call to ignore its result.


// The Correct, Idempotent Code
useEffect(() => {
  let ignore = false; // A flag to ignore the result
  console.log("Effect is running...");

  async function startFetching() {
    const data = await fetchUserData(userId);
    // Only update state if this effect is still "active"
    if (!ignore) {
      setUserData(data);
      console.log("State updated!");
    }
  }

  startFetching();

  // This is the cleanup function
  return () => {
    ignore = true; // Set flag to true on cleanup
    console.log("Cleanup function ran.");
  };
}, [userId]);

With this change, the first time the effect runs, ignore is false. Then React immediately unmounts it, which runs our cleanup function and sets ignore to true for that first scope. When the first API call eventually resolves, its if (!ignore) check will fail, and it won’t set the state. The second effect runs, its ignore flag remains false, and it safely updates the state. Problem solved, the right way.

Pro Tip: For more complex scenarios, especially when a user can trigger a new fetch before the old one completes, look into using an AbortController. It’s a more robust way to cancel in-flight network requests and is the gold standard for this kind of cleanup.

Solution 3: The “Zen” Fix – Is It Even a Problem?

This is the question that separates junior and senior engineers. Sometimes, you don’t need to change any code at all. The “fix” is to understand the behavior and correctly identify it as a non-issue.

Ask yourself: what is the actual side effect of this API call running twice?

Scenario Is it a problem?
A GET request to fetch read-only data (e.g., user profile). Probably not. It’s a harmless extra read operation that only happens in your local dev environment. Annoying, but not a bug.
A POST request that creates a new record in the database (e.g., initiate-session). YES, absolutely. This is a critical bug. You are creating duplicate data. Use Solution 2 immediately.
An analytics tracking event. YES. You are skewing your metrics and making bad business decisions based on faulty data. Fix it.

If your effect is just a simple GET request, the most pragmatic solution might be to simply acknowledge the development-only behavior, confirm it doesn’t happen in a production build (npm run build && npm start), and move on. Don’t add complexity to solve a problem that doesn’t exist in production.

At the end of the day, React.StrictMode is your friend. It’s a bit aggressive, but it’s trying to make you a better developer. Don’t fight it—learn from what it’s trying to teach you.

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ Why does my `useEffect` run twice in React development?

Your `useEffect` runs twice in development because `React.StrictMode` is active. It intentionally double-invokes effects (mount, run setup, unmount, run cleanup, remount, run setup again) to help you find side effects that lack proper cleanup and ensure your components are resilient.

âť“ How does `useEffect` cleanup compare to disabling Strict Mode?

Disabling `React.StrictMode` (Solution 1) is a hack that hides potential bugs and removes a valuable development tool. Implementing proper `useEffect` cleanup (Solution 2) addresses the root cause by making effects idempotent and robust, which is the recommended professional approach as it ensures your component behaves predictably in all scenarios.

âť“ What’s a common implementation pitfall when dealing with double `useEffect` calls?

A common pitfall is failing to implement a cleanup function for effects that cause critical side effects, such as `POST` requests that create new records or analytics events. This leads to duplicate operations, data inconsistencies, or skewed metrics. The solution is to use a flag (e.g., `ignore`) within the effect’s scope or an `AbortController` to prevent obsolete operations from completing or updating state.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading