Interview question
Prevent a duplicate form mutation
Guards a mutation immediately, exposes pending state, and restores the command after failure.
TL;DR
Guards a mutation immediately, exposes pending state, and restores the command after failure.
Synchronous guards, pending UI, retries, idempotency keys, and accessible feedback.
Practice the problem like a real interview: restate, reason, implement, and test.
The save command may take several seconds. Prevent duplicate calls before React renders the disabled state, while allowing retry after failure.
Two rapid submit events produce one fetch call and one stable idempotency key.
A ref closes the gap before the pending render, while state communicates progress. I also keep one attempt object containing the normalized command and idempotency key. A definitive rejection clears that attempt so corrected input can start a new operation; an ambiguous transport failure retains it, so Retry sends the same payload with the same key instead of risking a duplicate order.
type Attempt = {
key: string;
command: CreateOrderInput;
};
const submittingRef = useRef(false);
const attemptRef = useRef<Attempt | null>(null);
const [pending, setPending] = useState(false);
const [error, setError] = useState<string | null>(null);
async function submit(event: FormEvent) {
event.preventDefault();
if (submittingRef.current) return;
submittingRef.current = true;
setPending(true);
setError(null);
const attempt = attemptRef.current ?? {
key: crypto.randomUUID(),
command: normalizeOrder(values),
};
attemptRef.current = attempt;
try {
await api.createOrder(attempt.command, {
idempotencyKey: attempt.key,
});
attemptRef.current = null;
} catch (caught) {
if (isDefinitiveRejection(caught)) {
attemptRef.current = null;
setError("Review the order details and try again.");
} else {
setError("The result is uncertain. Retry to check the same order.");
}
} finally {
submittingRef.current = false;
setPending(false);
}
}
return <>
<button type="submit" disabled={pending} aria-busy={pending}>
{pending ? "Creating order" : "Create order"}
</button>
{error ? <p role="alert">{error}</p> : null}
</>;
isDefinitiveRejection should be based on the API client's typed outcome, not on guessing from a generic exception. While an outcome is uncertain, Retry reuses the captured command and key; changing it should be an explicit new operation after reconciliation.
The client guard improves UX; database-backed idempotency provides correctness if the browser retries after an uncertain response.
Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.