You add useEffect(..., []), open the network panel, and see two requests. The tempting fix is a ref named hasRun. That can silence the symptom while preserving the bug Strict Mode found.
Several live builds had a translation preloader that fetched twice on every development mount. React was doing this deliberately. In Strict Mode, development runs an extra setup → cleanup → setup cycle to expose effects that cannot be safely stopped and restarted.
The goal is not “make the effect run once.” React's own guidance is to make the user unable to distinguish one setup from setup-cleanup-setup.
Give every setup a cleanup
For a request tied to the mounted component, abort or ignore the obsolete result:
useEffect(() => {
const controller = new AbortController()
let active = true
void loadTranslations({ signal: controller.signal }).then(result => {
if (active) translationStore.replace(result)
})
return () => {
active = false
controller.abort()
}
}, [])
For a WebSocket, subscribe in setup and unsubscribe in cleanup. For a DOM listener, add and remove the same listener. For an animation, stop or reset it. Strict Mode is checking symmetry.
Aborting a fetch may still let the server see the first request. If duplicate reads are expensive, deduplicate them in a client-side cache or data-fetching layer. If the effect performs a write, the server needs an idempotency key; a component lifecycle is never an exactly-once delivery mechanism.
Know when you do not need an effect
If the work does not synchronize with an external system, move it out of the effect. Derive render data during render. Start a purchase from the click handler that represents the purchase. Load route data in the framework's data layer where caching and request ownership are explicit.
A useRef guard can be appropriate for a narrow UI bookkeeping case, but it is not a general Strict Mode fix. It may skip the real setup after the development cleanup, and it does nothing for a genuine later remount. It also hides whether the effect can release its resources.
Test remounting on purpose
Mount the component, navigate away, and return. Change the authenticated user or locale. Disconnect and reconnect the network. The preloader should not leak a stale response from the previous context, and a connection should not remain subscribed after unmount.
The duplicate translation request was low severity. The lesson is high leverage: an effect that only works when setup happens exactly once is relying on a promise React never made.
