July 6, 2026
When I Spent Hours Debugging a React Bug That Didn’t Even Exist.
You are sitting at your desk, happily coding a new React feature, and you decide to console.log() to check your state. You open up the…

By Shreyash
3 min read
You are sitting at your desk, happily coding a new React feature, and you decide to console.log() to check your state. You open up the browser console, trigger the action, and look down.
Instead of one log, you see two.
You click a button to increment a counter. The counter jumps by two.
Suddenly, panic sets in. Is my logic broken? Am I causing infinite loops? Is my entire component tree falling apart?
A few days ago, I became a victim of this exact scenario. I spent hours in absolute frustration, commenting out lines of code in every way imaginable just to see if anything would change. If you are currently tearing your hair out over the exact same problem, let me save you a massive headache and break down the two invisible culprits behind this madness.
The Debugging Nightmare: What Actually Happened
When my console started logging everything twice, I went into full panic mode. I started investigating my code down to the bare bones, convinced I had written a horrific bug. When my hours of debugging failed, I finally swallowed my pride and searched Google.
That is when I met the first culprit.
Culprit #1: React Strict Mode
Following a StackOverflow thread, I navigated into my index.js file (where the root component is initialized in the ReactDOM ). Right there, staring back at any default React boilerplate, was this:
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);I deleted
<React.StrictMode>, saved the file, and breathed a sigh of relief. Problem solved, right?
Wrong. The headache wasn't gone. My code was still double-executing.
Culprit #2: CodeSandbox's HMR "Double Evaluation"
Frustrated and exhausted, I dug deeper into Google. It turns out that because I had taken a quick shortcut to code directly in an online browser environment instead of setting up a local environment, I had run headfirst into a known trait: CodeSandbox's Hot Module Replacement (HMR) Double Evaluation.
When you use an empty dependency array [] in useEffect, it is supposed to run exactly once when the component mounts. However, CodeSandbox's HMR engine sometimes injects updated code on the fly without cleanly unmounting the older version of the component.
A tiny, background environment behavior cost me several hours of my life. That was the exact moment I realized: Yeah, I'm switching back to VS Code, installing my dependencies manually, and never running into this type of shit again.
Why the hell does React "Strict Mode" Do This?
Once the problem was solved, I had to ask: Why is Strict Mode intentionally designed to duplicate our code execution?
React Strict Mode is a development-only tool. It does not look or change anything in your production build. Its entire job is to be an aggressive inspector that intentionally double-invokes component renders, initializers, and effects to flush out bugs before they reach your users.
Specifically, it forces your code to run twice to catch:
- Impure Functions: Components should be pure. Given the same inputs (props), they should always render the exact same UI. If double-rendering a component changes your state or app behavior unexpectedly, you have a side-effect leaking out of your render logic.
- Memory Leaks: If your component sets up a global event listener, a WebSocket connection, or an interval, running it twice forces you to notice if you are stacking duplicate instances in memory.
The Real Fix: Accepting Strict Mode with Cleanup Functions
The solution to surviving Strict Mode isn't to just delete it from your project. The real, production-ready solution is to write resilient code by pairing your useEffect hooks with a cleanup function.
If your effect performs an asynchronous operation, sets an interval, or listens to an event, your component must clean up after itself when it unmounts.
Here is how you write a self-cleaning useEffect:
import { useEffect, useState } from 'react';
function EventComponent() {
useEffect(() => {
// 1. Setup the behavior
const handleScroll = () => console.log("Scrolling...");
window.addEventListener('scroll', handleScroll);
// 2. The Cleanup Function
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []); // Empty array = runs once on mount, cleans up on unmount
}import { useEffect, useState } from 'react';
function EventComponent() {
useEffect(() => {
// 1. Setup the behavior
const handleScroll = () => console.log("Scrolling...");
window.addEventListener('scroll', handleScroll);
// 2. The Cleanup Function
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []); // Empty array = runs once on mount, cleans up on unmount
}How the Cleanup Function Saves the Day
When React Strict Mode mounts, unmounts, and remounts your component in a fraction of a second, the sequence looks like this:
- The Effect Mounts
React runs your useEffect code and sets up the first scroll listener.
2. The Immediate Unmount
Strict Mode instantly simulates an unmount. It fires your cleanup function, completely wiping out the first scroll listener.
3. The Remount
React immediately mounts the component again, running your useEffect a second time to create a single, clean listener.
Because the cleanup function ran in step 2, you are left with exactly one active listener. No memory leaks, no ghost execution, and no double-logging.
The Lesson Learned
Shortcuts like Cloud Sandboxes are great for rapid prototyping, but nothing beats the reliability of your own local VS Code setup. More importantly, don't fear the double-render. If Strict Mode breaks your app, don't mute the warning by deleting the tag — fix the underlying leak with a cleanup function!