GitHub

Copyright © 2026 Sumit Paul

Design by Sumit Paul•LinkedIn

LinkedIn

Double submit caused by UI state lag

Avatar

Sumeet Kumar Paul

Frontend Engineer

2 min readSource code
Feb 16, 2026

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-ui-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-ui-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.

UI state updates are not synchronous execution guards. When side effects must not run more than once, protection must exist outside the render cycle. Immediate execution locks prevent duplicate network calls and inconsistent server state.

Keywords

double submit bug, frontend edge case, react button disable race, ui correctness, state safety

More Cases

2 min readFeb 20, 2026

State 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.

Avatar

Sumeet Kumar Paul

2 min readFeb 18, 2026

Optimistic 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.

Avatar

Sumeet Kumar Paul

2 min readFeb 13, 2026

Stale 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.

Avatar

Sumeet Kumar Paul

2 min readFeb 10, 2026

State update after unmount during navigation

When a component starts asynchronous work and the user navigates away before it resolves, the async callback may still attempt to update state. At that point, the component no longer exists in the UI tree.

Avatar

Sumeet Kumar Paul