Fix: React StrictMode Double Render — Side Effects Running Twice in Development
Quick Answer
How to fix React StrictMode double render issues — understanding intentional double invocation, fixing side effects, useEffect cleanup, external subscriptions, and production behavior.
The Problem
In development, a React component renders twice even though it’s called once:
function UserList() {
console.log('Rendering UserList');
// Logs appear twice: "Rendering UserList" "Rendering UserList"
const [users, setUsers] = useState([]);
useEffect(() => {
console.log('Fetching users...');
fetchUsers().then(setUsers);
// "Fetching users..." appears twice — two API calls are made
}, []);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}Or a component creates duplicate entries in a database or external system:
useEffect(() => {
trackPageView('/dashboard'); // Analytics event fires twice in development
registerDevice(deviceId); // Device registered twice — duplicates in backend
}, []);Or a subscription is set up twice, causing duplicate events:
useEffect(() => {
const ws = new WebSocket('wss://api.example.com/feed');
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
// Two WebSocket connections — each message appears twice
}, []);Why This Happens
React’s <StrictMode> intentionally invokes certain functions twice in development only. This behavior was introduced in React 18 and expanded in later versions.
What StrictMode double-invokes:
- Component render functions (the function body itself)
- State initializer functions (
useState(() => computeInitialState())) useReducerreducer functionsuseMemocallbacksuseEffectsetup AND cleanup functions — the effect runs, cleanup runs, then the effect runs again
Why React does this:
- Detect side effects in render functions (renders must be pure)
- Verify that
useEffectcleanup functions properly undo setup - Expose bugs where effects don’t clean up after themselves
Key facts:
- This ONLY happens in development mode with
<StrictMode>enabled - Production builds never double-invoke
- The second render is discarded — React uses the first render’s result
- For
useEffect, the sequence is:setup → cleanup → setup(React simulates unmount/remount)
Fix 1: Make Effects Idempotent
The correct fix for most cases — design effects so running them twice has the same result as running once:
// PROBLEM — fetch called twice, race condition possible
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers);
}, []);
// FIX — use an AbortController for cleanup
useEffect(() => {
const controller = new AbortController();
fetch('/api/users', { signal: controller.signal })
.then(r => r.json())
.then(setUsers)
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
}
});
// Cleanup: abort the first fetch before the second runs
return () => controller.abort();
}, []);
// StrictMode sequence:
// 1. First render: fetch starts
// 2. Cleanup: first fetch aborted (AbortError caught and ignored)
// 3. Second render: fresh fetch starts — this is the one that completesExternal subscription cleanup:
// PROBLEM — duplicate subscription
useEffect(() => {
const ws = new WebSocket('wss://example.com/feed');
ws.onmessage = (event) => setMessages(prev => [...prev, event.data]);
// No cleanup — StrictMode creates TWO WebSocket connections
}, []);
// FIX — close WebSocket in cleanup
useEffect(() => {
const ws = new WebSocket('wss://example.com/feed');
ws.onmessage = (event) => setMessages(prev => [...prev, event.data]);
return () => {
ws.close(); // StrictMode closes first connection before opening second
};
}, []);
// Result: only one active WebSocket at a timeFix 2: Fix useEffect with External Systems
Pattern for connecting to external systems:
// EventEmitter subscription
useEffect(() => {
function handleUpdate(data) {
setData(data);
}
emitter.on('update', handleUpdate);
// Cleanup removes listener — prevents duplicate listeners in StrictMode
return () => {
emitter.off('update', handleUpdate);
};
}, []);
// Redux / Zustand store subscription
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setState(store.getState());
});
return unsubscribe; // Cleanup: unsubscribe
}, []);
// Third-party library initialization
useEffect(() => {
const chart = new Chart(canvasRef.current, config);
return () => {
chart.destroy(); // Cleanup: destroy to allow clean re-initialization
};
}, []);Fix 3: Handle One-Time Effects Correctly
Some operations genuinely should only run once (analytics, initializers). Use a ref to track if the effect already ran:
// For analytics and other truly-once operations
import { useEffect, useRef } from 'react';
function AnalyticsPage({ pageName }) {
const tracked = useRef(false);
useEffect(() => {
if (tracked.current) return; // Skip second invocation
tracked.current = true;
analytics.trackPageView(pageName); // Fires only once
}, [pageName]);
return <div>...</div>;
}Note: This pattern works but consider whether the operation truly can’t tolerate cleanup/re-run. If
pageNamechanges, the ref prevents tracking the new page. Make sure to handlepageNamechanges correctly:
useEffect(() => {
analytics.trackPageView(pageName); // Fire each time pageName changes — is idempotent
// No cleanup needed — tracking a page view doesn't need to be undone
}, [pageName]);
// This is actually fine without the ref — tracking fires once per pageName change
// The StrictMode double-fire on mount is usually acceptable for analyticsFix 4: Fix State Initialization Side Effects
State initializers run twice in StrictMode. Keep them pure:
// WRONG — side effect in state initializer
const [connection, setConnection] = useState(() => {
const ws = new WebSocket('wss://example.com'); // Opens TWO connections in StrictMode
return ws;
});
// WRONG — expensive side effect in state initializer
const [data, setData] = useState(() => {
fetchData(); // Called twice in StrictMode
return null;
});
// CORRECT — pure computation only in state initializer
const [items, setItems] = useState(() => {
return JSON.parse(localStorage.getItem('items') || '[]'); // Pure read — fine
});
// CORRECT — side effects go in useEffect, not useState
const [connection, setConnection] = useState(null);
useEffect(() => {
const ws = new WebSocket('wss://example.com');
setConnection(ws);
return () => ws.close(); // Proper cleanup
}, []);Fix 5: React Query and SWR — No Double Fetch Problem
Libraries like React Query and SWR handle deduplication automatically — they don’t fire duplicate network requests even with StrictMode:
import { useQuery } from '@tanstack/react-query';
function UserList() {
// Only ONE network request made, even in StrictMode
// React Query deduplicates requests and caches results
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}Using SWR:
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(r => r.json());
function UserList() {
// SWR also deduplicates — single request in StrictMode
const { data: users, error } = useSWR('/api/users', fetcher);
// ...
}Fix 6: Check if StrictMode Is Causing the Issue
Verify whether StrictMode is the cause before applying fixes:
// Temporarily disable StrictMode to confirm it's the cause
// (Don't leave this disabled in production code)
// Before:
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// After (for debugging only):
root.render(<App />);
// If the double-render stops, StrictMode was the cause
// Fix the underlying issue instead of removing StrictModeAdd logging to understand the render cycle:
function UserList() {
const renderCount = useRef(0);
renderCount.current += 1;
console.log(`Render #${renderCount.current}`);
useEffect(() => {
console.log('Effect setup');
return () => {
console.log('Effect cleanup');
};
}, []);
return <div>...</div>;
}
// In StrictMode development, output is:
// Render #1
// Render #2 ← Discarded — React uses render #1's output
// Effect setup ← First setup
// Effect cleanup ← StrictMode cleanup
// Effect setup ← Second setup — this is the active effectFix 7: Production Behavior vs Development Behavior
Understanding the difference helps set correct expectations:
// In PRODUCTION:
// - Component renders once
// - useEffect fires once
// - No cleanup/re-run cycle
// In DEVELOPMENT with StrictMode:
// - Component function called twice (second result discarded)
// - useEffect: setup → cleanup → setup
// - useState initializer called twice (second result discarded)
// Code that works correctly in StrictMode will work correctly in production
// Code that only "works" by removing StrictMode has a latent bug
// EXAMPLE — bug revealed by StrictMode
useEffect(() => {
const handler = () => setCount(prev => prev + 1);
window.addEventListener('resize', handler);
// MISSING CLEANUP — in production, one listener added (works)
// In StrictMode, two listeners added (count increments twice per resize)
// Fix: return () => window.removeEventListener('resize', handler);
}, []);Still Not Working?
useInsertionEffect — runs synchronously before DOM mutations. Only used for CSS-in-JS libraries. Does NOT double-fire in StrictMode.
useLayoutEffect — runs synchronously after DOM mutations, before the browser paints. Does double-fire in StrictMode. Must also have proper cleanup.
Third-party libraries not StrictMode-compatible — some older libraries weren’t designed with StrictMode in mind. They may not support being mounted/unmounted/remounted. Check the library’s documentation or issues for <StrictMode> compatibility.
React 18 vs React 17 StrictMode — React 18 added the unmount/remount behavior for useEffect. In React 17, StrictMode only double-invoked the render function, not effects. If you recently upgraded to React 18, new StrictMode behavior may surface previously hidden bugs.
For related React issues, see Fix: React useEffect Infinite Loop and Fix: React Hydration Error.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
Fix: Embla Carousel Not Working — Slides Not Scrolling, Autoplay Not Starting, or Thumbnails Not Syncing
How to fix Embla Carousel issues — React setup, slide sizing, autoplay and navigation plugins, loop mode, thumbnail carousels, responsive breakpoints, and vertical scrolling.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.