Forms Required
Save each form only once
Make a double click, a second Enter or a retry save the record once. Block repeat submits in the form and reject duplicates on the server.
How to do it
While a submit is in flight, ignore further submits and show that it is saving. Keep the button focusable. In React 19, read pending from useFormStatus or isPending from useActionState. Create one idempotency key when the form opens and send it with every attempt, as idempotency keys describes. Make a new key only after a confirmed save, if the form stays open for another record. After a server-rendered post, redirect with 303, so a reload doesn't post again. In htmx, hx-disabled-elt and hx-sync block repeats.
Example
export function submitOnce(form: HTMLFormElement, send: (data: FormData, key: string) => Promise<void>) {
const idempotencyKey = crypto.randomUUID();
let isSending = false;
form.addEventListener('submit', async event => {
event.preventDefault();
if (isSending) return;
isSending = true;
form.setAttribute('aria-busy', 'true');
try { await send(new FormData(form), idempotencyKey); }
finally { isSending = false; form.removeAttribute('aria-busy'); }
});
}
@app.post("/api/invoices")
def create_invoice_once():
key = request.headers.get("Idempotency-Key")
if not key:
return problem(400, "idempotency-key-required")
previous = idempotency.find(current_user().id, key)
if previous:
return previous.response
return idempotency.store(current_user().id, key, invoices.create_from(request.json))
How to check
A browser test clicks submit three times within 100 ms, and a second test sends the same request twice with one key. Each test creates exactly one record.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers app interface playbook: https://agentwranglers.se/playbook/app-ui/forms/single-submit.