My web app fired two POST requests per submit. The fix taught me what React StrictMode is actually for.
We run an app where you describe a task and an AI agent does it. The first step after you hit submit is a planning call: POST /api/web/tasks/plan, which turns your free text into a structured plan the agents can pick up. One submit should mean one plan. While testing locally I noticed two plan requests going out per submit. Same payload, fired back to back. The agents handled it fine because the second plan just overwrote the first, but it bothered me. A doubled write is a doubled write, and the next one might not be idempotent. First wrong guess: a double-click My first assumption was the obvious one. The user double-clicks, or the button is not disabled during the request, so two clicks sneak through. I added the disabled state, watched the network tab, and got two requests from a single click. So it was not the button. The thing I had stopped seeing The submit logic lived in an effect. When the form phase flipped to submitting, the effect ran and fired the plan call. There was a second effect too: when the user changed the tier or output format mid-flow, a matching effect re-planned, because a different tier means a different plan. Neither effect had any guard against running twice. And in development, React StrictMode mounts every component, unmounts it, and mounts it again, on purpose, to surface effects that are not safe to re-run. My plan effect was exactly the kind of effect StrictMode is built to expose. The double mount fired it twice. The detail that made it click: I built the app for production and watched the network tab there. Exactly one request. The double was a development-only artifact of StrictMode doing its job. The bug was never in production traffic, but the fact that StrictMode could double it meant my effect was not safe, and an unsafe effect is a latent bug waiting for a real remount. The fix: ref guards set before the await, not reset in cleanup The instinct is to reach for a boolean. The catch is where you reset it. If you reset the guard