Why does my useEffect run twice in Strict Mode?
What interviewers are testing
Interviewers want to see whether you understand why React 18 StrictMode double-invokes effects — mount, simulated unmount, remount — and whether you reach for the correct fix. The tell is whether a candidate disables StrictMode to silence a symptom or writes an effect that survives being set up and torn down twice.
Mental model
In development, StrictMode deliberately mounts, cleans up, and remounts every component to prove your effects are resilient. Effects must be idempotent: setup and cleanup should be able to run in any order and any number of times. Production invokes the effect exactly once, so code that only works because it never cleaned up is a latent bug.
Step-by-step solution
Step 1 of 5
The duplicated network calls
Open a fresh React 18 app, put a fetch or a subscription inside useEffect, and the network tab shows the work twice — while the production build shows it once. Candidates usually blame the fetch, the cache, or the dev server. The real trigger is StrictMode, which React enables in development templates. Watch the first animation: React renders the component twice before any effect runs, then the console prints subscribe, unsubscribe, subscribe in a tight rhythm and goes quiet. That rhythm is the fingerprint of an intentional effect setup, cleanup, and setup cycle. The important question is not how to silence the duplicate log — it is why React insists your effect survive a teardown it never asked you to write. If the effect is correct, the double invoke is harmless; if it is not, StrictMode has just exposed a bug that a real unmount or dependency change would eventually trigger in production.
Animation — The duplicated network calls
useEffect(() => { console.log('subscribe'); const sub = chat.subscribe(roomId, onMessage); return () => { console.log('unsubscribe'); sub.unsubscribe(); };}, [roomId]);Variables
First render: the component body runs before any effect.
Edge cases & traps
- State from the first run clobbers the second: abort the stale request (AbortController) or ignore it with a canceled flag in cleanup.
- Not resetting refs, intervals, or third-party widgets in cleanup: return a teardown that reverses every side effect the setup started.
- Removing StrictMode to hide the duplicate: the missing cleanup is still a bug and will fire on route changes and dependency changes in production.
- Guarding setup with a mountedRef: refs survive the simulated remount, so the second setup is skipped after the first was cleaned up — leaving zero live subscriptions.
- Double-counting analytics events in development: decide deliberately which events must fire once and dedupe them outside the effect rather than disabling the check.