React.js, part 2: why your effect runs twice in development (and what StrictMode is really telling you)
Part 2from the React.js series · 20 parts in all
Every developer meets it the same way: a console.log inside
useEffect fires twice, or a websocket opens two connections, and the first
instinct is a bug in React. It is not. In React 18 the development build mounts, unmounts,
and remounts every component, on purpose. Part 2 explains what that is testing and how to
make your effects pass.
The double invocation is a lint for missing cleanup
React is checking one property: is your component resilient to being torn down and set back up? Components can be removed and restored for reasons that do not involve you — a Suspense boundary re-showing a child, an offscreen tree being parked. If an effect's setup adds something the cleanup does not remove, the second mount exposes it as a duplicate.
useEffect(() => {
const socket = new WebSocket('wss://example.com/feed');
// Symmetric cleanup: StrictMode mounts, unmounts, then mounts again,
// so without this close() you would leak the first socket.
return () => socket.close();
}, []);
The other half: aborted fetches, not cancelled flags
The classic "is mounted" boolean is the anti-pattern. The cleanup should actually cancel
the work, which is what AbortController is for:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/items?q=${query}`, { signal: controller.signal })
.then(r => r.json())
.then(setItems)
.catch(err => {
if (err.name === 'AbortError') return; // expected on cleanup
setError(err);
});
return () => controller.abort();
}, [query]);
With a real cancellation the effect is safe under any number of mounts, and you get the
production benefit too: a fast-changing query stops firing stale requests.
What StrictMode deliberately does not do
It changes nothing in production, and it does not unmount your tree to save memory. The
double-render applies only in development, only to components under
<StrictMode>. The reason to keep it on is simple: it finds the effects that
only break in the field — the leaked socket, the timer that fires after unmount, the
subscription that doubles on every navigation.
Two habits follow. First, effects should be self-contained: acquire, then release.
Second, prefer data-fetching libraries that already implement cancellation rather than
reinventing it. Next: keeping the UI responsive with Suspense and
useTransition.