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. 1.In development with Strict Mode:
  2. 2.Component mounts -> effect runs -> fetch starts
  3. 3.Component unmounts -> cleanup runs -> fetch aborted
  4. 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

javascript
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:

javascript
// 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.