The Problem
In React 18 with Strict Mode enabled, every component mounts, unmounts, and mounts again during development. Effects run twice. This is intentional but causes confusion: API calls fire twice, subscriptions are created twice, and developers think something is broken.
Symptoms
- API requests fire twice in development (but only once in production)
- Console logs appear duplicated
- WebSocket connections open and immediately close, then reopen
- useEffect cleanup runs immediately after setup
- "Cannot set properties on null" errors in cleanup
Real Behavior
```javascript useEffect(() => { console.log('Effect runs'); // Logged twice in dev const controller = new AbortController();
fetch('/api/data', { signal: controller.signal }) .then(res => res.json()) .then(data => console.log('Data:', data)); // Fetched twice in dev
return () => { console.log('Cleanup runs'); // Logged twice in dev controller.abort(); // First request aborted immediately }; }, []); ```
- 1.In development with Strict Mode:
- 2.Component mounts -> effect runs -> fetch starts
- 3.Component unmounts -> cleanup runs -> fetch aborted
- 4.Component mounts again -> effect runs -> fetch starts (this one completes)
This Is Not a Bug
React does this intentionally to verify that your effects are resilient to mount/unmount/mount cycles. In production, effects only run once.
How to Handle It Properly
Fix 1: Proper Cleanup (React's Intended Solution)
```javascript useEffect(() => { const controller = new AbortController();
fetch('/api/data', { signal: controller.signal }) .then(res => res.json()) .then(data => setData(data)) .catch(err => { if (err.name !== 'AbortError') console.error(err); });
return () => controller.abort(); // Clean up on unmount }, []); ```
With proper cleanup, the first fetch is aborted when Strict Mode unmounts the component. Only the second fetch completes.
Fix 2: Use React Query or SWR
```javascript import { useQuery } from '@tanstack/react-query';
function DataComponent() { const { data, isLoading } = useQuery({ queryKey: ['data'], queryFn: () => fetch('/api/data').then(r => r.json()) });
// React Query deduplicates and caches requests automatically if (isLoading) return <Spinner />; return <div>{data.value}</div>; } ```
Fix 3: Track Initialization with useRef
useEffect(() => {
if (typeof window !== 'undefined' && !window.__appInitialized) {
window.__appInitialized = true;
initializeAnalytics(); // Only runs once even with Strict Mode
}
}, []);Should You Disable Strict Mode?
You can, but you shouldn't:
// DON'T do this in production
const root = ReactDOM.createRoot(document.getElementById('root'), {
// onRecoverableError: (err) => {} // Don't suppress errors
});Strict Mode catches real bugs. The double-invocation is a feature, not a problem to solve. Write effects that handle cleanup correctly, and the double-run becomes invisible.