🧩 One Extra Click, Two Orders Placed
A form submit button that’s still clickable while the request is in flight lets an impatient double-click fire the same form twice — on a checkout or payment form, that’s not a cosmetic bug, it’s a duplicate charge.
🐞 The Problem
<form id="checkoutForm" action="/checkout" method="POST">
<!-- fields... -->
<button type="submit">Place Order</button>
</form>
<!-- Nothing disables the button on submit - a fast double-click
(or even one impatient re-click while the page is loading)
submits the form twice, creating two orders. -->
🔍 Why This Happens
Submitting a form doesn't disable its own button by default, and the network request + page navigation/response takes real time - often several hundred milliseconds, easily enough for a second click to register before the button visually indicates anything happened.
✅ The Fix: Disable the Button the Instant Submit Fires
- A single ‘submit’ event listener that disables the button immediately closes the entire window for a double submission.
- Works whether the form does a traditional POST or an Ajax submission – the disable happens before either kind of request goes out.
🔒 Correct Version
const form = document.getElementById('checkoutForm');
const button = form.querySelector('button[type="submit"]');
form.addEventListener('submit', () => {
button.disabled = true;
button.textContent = 'Placing order...';
// For a traditional (non-Ajax) form, the page will navigate away
// shortly anyway; for an Ajax submission, re-enable it in your
// response handler if the request fails, so the user can retry.
});
⚠️ Don’t Rely on Client-Side Alone
- Always ALSO add server-side idempotency protection (an idempotency key, or checking for a very recent duplicate order) — a disabled button doesn’t stop a replayed request from a slow connection retry, a browser extension, or a user with JavaScript disabled.
- The client-side fix is about user experience and preventing the common case; the server-side fix is what actually guarantees correctness.
Disabling the button is the visible half of the fix and the one users actually feel — the server-side idempotency check is the half that keeps a rare edge case from ever becoming a real duplicate charge.
