- The symptom
- One impatient double click sends the same request twice.
- The cause
- The guard depends on rendered state, which can trail behind user input.
- The fix
- Take a synchronous lock in a ref before any side effect starts.
Problem
A submit button is disabled only after state updates to 'loading'. Under rapid user interaction, multiple clicks can occur before the UI visually reflects the disabled state.
- User clicks button rapidly.
- State update is scheduled but not yet committed.
- Multiple submit handlers execute.
- Duplicate API calls are triggered.
1const [loading, setLoading] = useState(false);
2
3function handleSubmit() {
4 if (loading) return;
5
6 console.log("[BROKEN] submit triggered");
7 setLoading(true);
8
9 void (async () => {
10 await delay(800);
11 console.log("[BROKEN] request finished");
12 setLoading(false);
13 })();
14}
15
16<button disabled={loading} onClick={handleSubmit}>
17 {loading ? "Submitting..." : "Submit"}
18</button>Because state updates are asynchronous, multiple click events can be processed before the button becomes disabled in the DOM.
Solution
Use a synchronous guard that does not rely on React render timing. A mutable ref blocks duplicate execution immediately before state updates propagate.
1const [loading, setLoading] = useState(false);
2const inFlightRef = useRef(false);
3
4function handleSubmit() {
5 if (inFlightRef.current) {
6 console.log("[FIXED] duplicate submit prevented");
7 return;
8 }
9
10 inFlightRef.current = true;
11 console.log("[FIXED] submit triggered");
12 setLoading(true);
13
14 void (async () => {
15 await delay(800);
16 console.log("[FIXED] request finished");
17 inFlightRef.current = false;
18 setLoading(false);
19 })();
20}
21
22<button disabled={loading} onClick={handleSubmit}>
23 {loading ? "Submitting..." : "Submit"}
24</button>The ref acts as an immediate execution lock. It prevents duplicate side effects before React re-renders and disables the button.
Check your own code
- 01What is the damage if this action runs twice?
- 02Is the lock released on failure as well as on success?
- 03Does the server also reject duplicate submissions?
- double submit bug
- frontend edge case
- react button disable race
- ui correctness
- state safety

Written by Sumeet Kumar Paul
Senior Frontend Engineer from India. Frontend Lab is where I reproduce real interface bugs and work through the fix.
Need a hand?
I build frontends like this for a living.
More from the lab
Sumeet Paul· 2 min readState update after route transition
A component starts asynchronous work and then a route transition occurs before the work completes. When the async task resolves, it updates state that no longer matches the active route.
Sumeet Paul· 2 min readOptimistic UI rollback failure
An optimistic UI update immediately reflects a user action before the server confirms success. If the request fails or multiple toggles occur quickly, the UI can drift away from actual server state.
Sumeet Paul· 2 min readStale closure in click outside listener
Event listeners defined inside effects capture state from the render during which they were created. If the listener is attached once and depends on state that later changes, it may continue operating on outdated values.