In development, StrictMode intentionally mounts a component, runs its effects, unmounts it, then mounts it again. Effects therefore run twice. This is a diagnostic, not a bug: it exposes effects that are not resilient to being re-run, which is the same situation a user hits on navigation or fast remounts.
Before you start
You should be comfortable with useEffect and cleanup functions. This article explains development-only behavior; production runs each effect once.
Step-by-step walkthrough
Step 1: Expect the mount-unmount-mount cycle
StrictMode’s double invocation means an effect runs, its cleanup runs, and the effect runs again. If the second run duplicates a subscription or a network request, the effect is missing cleanup or an idempotency guard, and the same problem would appear in production on remount.
Step 2: Make every effect clean up after itself
Return a cleanup that cancels subscriptions, aborts fetches and clears timers. A correct effect is safe to run repeatedly because each run’s side effects are undone by the paired cleanup, so the double invocation produces no lasting duplicate.
Step 3: Use the double run to find bugs, not to opt out
Removing StrictMode hides real defects that will resurface in production. Instead, treat a duplicated request or a lost subscription in development as a signal to fix the effect. Guard against races with an abort or a cancellation flag rather than assuming the effect runs once.
Worked scenario
The effect aborts its request on cleanup, so the second run is harmless.
useEffect(() => {
const controller = new AbortController();
fetch(`/api/items`, { signal: controller.signal })
.then((res) => res.json())
.then(setItems)
.catch((error) => {
if (error.name !== 'AbortError') setError(error);
});
return () => controller.abort();
}, []);Walk through the example
On the first run the fetch starts; StrictMode then runs the cleanup, which aborts it, and the effect runs again with a fresh controller. The aborted request rejects with an AbortError, which is ignored, so it cannot overwrite the second request’s result. This is exactly the pattern that keeps the UI correct on a real remount.
Common mistake
Disabling StrictMode to stop the double calls, which removes a safety net and leaves the underlying leak unfixed. Another is using a module-level hasRun flag to skip the second run, which breaks in production when the component genuinely remounts.
Verify the behavior
Log inside the effect and its cleanup and confirm the mount-unmount-mount order. Confirm only one request is active at a time by aborting on cleanup. Remove the cleanup and observe the duplicate subscription or race, which is the bug StrictMode is designed to expose.
Interview exercise
An effect subscribes in development twice but once in production. Is the code wrong?
Answer and reasoning
It is a warning sign, not necessarily wrong: production runs the effect once, so no duplicate appears there. But the code should still clean up the subscription, because any real remount would duplicate it. Add a cleanup that unsubscribes; then the double invocation is harmless and the same code is safe under the remounts production actually produces.
Continue learning
Compare effect correctness in Effect cleanup and updater purity. Read the React StrictMode reference and try the React interview questions.