This is intentional in React 18+ with Strict Mode enabled. React mounts, unmounts, and remounts components in development to help you find side-effect bugs.
Why does React do this?
To simulate components being unmounted and remounted (e.g., user navigating away and back). It ensures your effects are “resilient” – i.e., they clean up properly and don't assume they only run once.
How to fix
Keep it – it's harmless once you add proper cleanup.
1
2useEffect(() => {
3
4 const controller = new AbortController();
5
6 fetch('/api', { signal: controller.signal })
7
8 .then(res => res.json())
9
10 .then(setData);
11
12
13
14 return () => controller.abort(); // cleanup cancels duplicate
15
16}, []);
17
1
2useEffect(() => {
3
4 const controller = new AbortController();
5
6 fetch('/api', { signal: controller.signal })
7
8 .then(res => res.json())
9
10 .then(setData);
11
12
13
14 return () => controller.abort(); // cleanup cancels duplicate
15
16}, []);
17
Turn off Strict Mode (not recommended) in index.js:
1
2// <React.StrictMode> ← comment this out
3
4 <App />
5
6// </React.StrictMode>
7
1
2// <React.StrictMode> ← comment this out
3
4 <App />
5
6// </React.StrictMode>
7
Use a ref to track first run (workaround, but not idiomatic):
1
2const hasRun = useRef(false);
3
4useEffect(() => {
5
6 if (!hasRun.current) {
7
8 hasRun.current = true;
9
10 // your effect
11
12 }
13
14}, []);
15
1
2const hasRun = useRef(false);
3
4useEffect(() => {
5
6 if (!hasRun.current) {
7
8 hasRun.current = true;
9
10 // your effect
11
12 }
13
14}, []);
15
✅ Takeaway: Design effects to be startable/cleanup-able multiple times. This future-proofs your app for concurrent rendering features.