Skip to content

Blog / Double Submit Bug

Double submit caused by UI state lag

Sumeet Kumar Paul2 min read
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.
page.tsx
srcapppracticedouble-submit-caused-by-ui-state-lagpage.tsx
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.

page.tsx
srcapppracticedouble-submit-caused-by-ui-state-lagpage.tsx
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

  1. 01What is the damage if this action runs twice?
  2. 02Is the lock released on failure as well as on success?
  3. 03Does the server also reject duplicate submissions?
  • double submit bug
  • frontend edge case
  • react button disable race
  • ui correctness
  • state safety
View the source on GitHub ↗

Written by Sumeet Kumar Paul

Senior Frontend Engineer from India. Frontend Lab is where I reproduce real interface bugs and work through the fix.

Share article:

Need a hand?

I build frontends like this for a living.

Get in touch

More from the lab