Quick Summary
React Strict Mode helped uncover six silent bugs in a client’s React 18 dashboard including duplicate API calls, memory leaks from missing cleanup, analytics events firing during render, and incompatible third-party libraries. Every fix followed the same principle make effects idempotent, always return cleanup functions, and use functional state updates. The biggest takeaway is that Strict Mode doesn’t create bugs, it just forces the ones already hiding in your code to surface in development before they cause real damage in production.
Table of Contents
React Strict Mode is a development only tool that assists developers to detect the potential problems in their application in the development process. It’s not the typical debugging tool as it does not modify the UI or affect the production builds. It runs extra checks, triggers specific lifecycle behaviors, and flags unsafe coding practices.Â
Six months of performance complaints. When a client came with a React application that seemed slow, we expected that it had a huge bundle, an unoptimized image, or missing React.memo, but we found silent bugs that had been residing in the codebase for months. Duplicate API calls, memory leaks, side effects firing during render none of them causing the app crash, but collectively bringing the user experience to a pause.
The turning point came when we added the React strict mode, it didn’t just surface warnings it actively showed the bugs that would have failed under specific timing, network, or state conditions in production. So, let’s discuss exactly what we found, how React Strict Mode surfaced each issue, and the systematic process we used to fix them.
Our client’s application was on React 18 version, its logistics operation, real time shipment tracking app with dashboard of analytical chart, and live feed with driver location. The team had been facing performance issues for over a month. Let’s discuss the breakdown of every bug that React strict mode helps us to solve.
The most noticeable symptom was the network panel showing every API call triggered twice on initial load. The root cause was a useEffect that kicked off data fetching without any cleanup or abort mechanism. In React 18’s Strict Mode, effects are purposefully unmounted and remounted if you don’t cancel ongoing requests during cleanup, you receive precisely two calls:
// This fires twice in Strict Mode and in concurrent React
useEffect(() => {
fetchShipments().then(data => setShipments(data));
}, []);
Why this is dangerous in production:
In concurrent mode, React is able to pause and resume rendering processes. An unprotected fetch might complete after the component has unmounted, invoking setState on a non-existent component that results in either outdated data being shown or unnoticed memory leaks.
Multiple elements establish WebSocket listeners, timers, and event subscriptions without ever tearing them. Whenever a component was unmounted and subsequently remounted (as mandated by Strict Mode during development), a new listener was added while the previous one remained. Throughout a session, this resulted in multiple orphaned listeners all modifying the same state.
// Each mount adds a new listener, none are removed
useEffect(() => {
const socket = new WebSocket('wss://api.example.com/live');
socket.addEventListener('message', handleUpdate);
// No cleanup! No socket.close(), no removeEventListener
}, []);
A less obvious bug involved a component that initiated an analytics event directly within its render body, bypassing any useEffect. Render functions must be pure (no observable external interactions), so React Strict Mode invokes them twice to identify this specific pattern. As a result each page view was recorded two times in their analytics system, thoroughly corrupting their funnel data.
function ShipmentDashboard() {
// Side effect directly in render fires on every re-render
analytics.track('dashboard_viewed');
return ...;
}
Multiple components were invoking setState with values based on the existing state, but they were utilizing the closure value rather than the functional update approach. In a synchronous rendering environment, this generally functions well, but Strict Mode’s double-render revealed situations where the UI flickered between two states as each render calculated the update from an outdated snapshot.
Several components establish
// Uses stale `count` from closure breaks under double-render
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // stale closure!
};
A chart library component located deep in the component tree was still utilizing componentWillReceiveProps, a lifecycle method that has been deprecated since React 16.3 and entirely eliminated in React 18. As it was within a third-party component, the team had not detected it before. Strict Mode revealed it with a distinct console warning that pointed straight to the component tree.
Console warning in Strict Mode: Warning: componentWillReceiveProps has been renamed, and is not recommended for use. See https://reactjs.org/link/unsafe-component-lifecycles for details.
A significant challenge involved a drag-and-drop library that held DOM element references internally upon first mounting, expecting those references to stay consistent. When Strict Mode unmounted and remounted the component tree, the library’s internal DOM references grew outdated, leading to entirely dysfunctional drag behavior in development and, crucially, demonstrating that the library wouldn’t be safe in React 18’s concurrent environment. Â
Key insight:
If a third-party library fails exclusively due to Strict Mode’s double-mount behavior, it strongly indicates that the library is not safe for concurrent mode and must be either replaced or patched before completing your React 18 migration.Â
Hire Reactjs developer who know how to deliver real results. From debugging and performance tuning to full-scale development, we’re here to help you every step of the way.
Without the Strict Mode, all of these errors were almost undetectable during regular development. The app started, pages displayed, data showed up everything seemed fine until you closely examined the network tab or saw your analytics figures were off.Â
Strict Mode turned hidden errors into prominent alerts via three distinct mechanisms:
Double render invocation: Triggered the analytics event to activate twice with each mount, ensuring it can’t be overlooked during development.Â
Effect mount → unmount → remount cycle — Surfaced duplicate API calls and leaked WebSocket listeners immediately on first page load in development.
Deprecated API warnings: Printed actionable, stack-traced warnings to the console pointing directly at the offending component, even inside third-party code.Â
The key takeaway is that Strict Mode doesn’t create these bugs it simply causes them to occur immediately and reliably during development rather than intermittently in production. Each double-render and each invocation of a double effect is React communicating: “If your code isn’t equipped to manage this, it will fail in concurrent mode.” “fix it immediately.”
After Strict Mode provided us with a clear list of issues, we worked them methodically. Here’s precisely how we tackled each type of bug.
The primary principle: always avoid causing side effects while rendering. All interactions with the outside environment API requests, analytics, DOM changes, timers should be placed within a useEffect. We moved all analytics calls into effects with relevant dependency arrays:
function ShipmentDashboard() {
// Side effect moved into useEffect fires once after mount
useEffect(() => {
analytics.track('dashboard_viewed');
}, []); // Empty array = run once on mount
return ...;
}
Each useEffect that establishes a subscription, listener, timer, or connection must provide a cleanup function upon return. This is obligatory in React 18. We upgraded all current effects with appropriate teardown mechanisms.
// WebSocket connection with full cleanup
useEffect(() => {
const socket = new WebSocket('wss://api.example.com/live');
socket.addEventListener('message', handleUpdate);
// Cleanup: close socket and remove listener on unmount
return () => {
socket.removeEventListener('message', handleUpdate);
socket.close();
};
}, [handleUpdate]);
We utilized the AbortController pattern for API calls to terminate ongoing requests when a component is unmounted. This is the appropriate solution for React 18’s concurrent model that does not eliminate Strict Mode and does not rely on refs to monitor mount state as a workaround.
// Fetch with abort on cleanup handles Strict Mode + concurrent React
useEffect(() => {
const controller = new AbortController();
fetchShipments({ signal: controller.signal })
.then(data => setShipments(data))
.catch(err => {
if (err.name !== 'AbortError') throw err;
// AbortError is expected — swallow it silently
});
return () => controller.abort(); // Cancel on unmount
}, []);
We replaced all closure-based state updates with the functional version of setState, which consistently gets the most current state value no matter when the update occurs, completely removing stale closure issues.
// Functional update — always uses the latest state
const increment = () => {
setCount(prev => prev + 1); // prev is always current
};
For complex state entangled in numerous co-dependent useState calls, we merged into useReducer, which clarifies and allows testing of state transitions.
For the chart library utilizing componentWillReceiveProps, we initially verified if an update was present. Upgrading to the newest major version completely fixed the problem. The drag-and-drop library lacked a concurrent-safe version, so we replaced it with a new alternative designed with React hooks from the start.
Decision framework for third-party library issues: Check for an update first → if fixed, upgrade. If not fixed, check if it's maintained → if abandoned, replace. If actively maintained, open an issue and apply a wrapper component as a temporary shim.
React Strict Mode is most beneficial when you consider it a constant element of your development environment, rather than a temporary diagnostic tool that you enable when an issue arises. Here are the best practices we implement on every React 18 project.
The expense of integrating Strict Mode compliance into a significant legacy codebase is largely greater than developing with it activated from the beginning.
Retrofitting React Strict Mode compliance into a large legacy codebase costs far more than building with it on from day one.
Enclosing a subtree within a non-Strict component to hide warnings is accruing future liabilities. Resolve warnings at their origin.
Every useEffect that listens to anything should include a return function. Create a lint rule utilizing eslint-plugin-react-hooks.
Before using any new library, conduct a brief test in Strict Mode. Conflicts are much less expensive to resolve before they become integrated into the codebase.
Combine Strict Mode with eslint-plugin-react-hooks to identify missing dependencies and exhaustive dependencies problems during writing, rather than during execution.
Create effects that ensure executing them two times consecutively yields the same result as executing them once. This is the contract React needs for concurrent rendering.
Pipe React’s development-mode warnings into your CI testing suite. A zero-tolerance approach in development leads to a more reliable production application.Â
For teams onboarding to React 18, the noticeable feedback loop from Strict Mode is the quickest method to develop an understanding of the concurrent rendering framework.
The most significant change following this involvement wasn’t the number of bugs. It was the way the team viewed effects. Once you recognize that React Strict Mode is a quicker, more noticeable preview of what concurrent rendering will ultimately reveal in production, you stop resisting the double mounts and begin crafting code that accommodates them. Idempotent effects, real cleanup, functional state updates these no longer seem like rules and instead appear to be the sole reasonable approach to developing for React 18 and future versions.
With React 19 already shipping the compiler and even tighter concurrent semantics, this work isn’t optional debt cleanup, it’s the floor. If your team is mid-migration or auditing a legacy React app, partnering with an experienced React js development company is the fastest way to get the warnings cleared without stalling your roadmap.
No. Strict Mode only runs in development builds. It’s stripped out of production bundles entirely, so end users never see double-mounts, double-renders, or any of its checks.
React 18’s Strict Mode intentionally mounts, unmounts, and remounts components in development to verify your effects can survive the concurrent-mode lifecycle. If your component breaks, it’s not a Strict Mode bug it’s a hidden bug Strict Mode just exposed.
No. Disabling it just hides bugs that will eventually fail in production under concurrent rendering. The right move is to fix the underlying effect, not silence the warning.
Mount it inside a <StrictMode> wrapper in development. If it breaks on remount broken refs, lost state, dead event handlers it’s not concurrent-safe. Check for a maintained version, or replace it.
Yes. Wrap only the component subtree you want to test. This is useful for incremental migration of legacy codebases, turn it on for new features first, then expand outward as you fix older code.