Networks can fail in an awkward way. A request reaches the server, the server saves the order and takes the payment, and then the answer is lost on its way back. The page only sees Failed to fetch, so the visitor clicks Pay again. Other visitors simply double-click.
Two guards make one click one order. First, the button is disabled while the request is out. It is turned back on in a finally block, which runs after the try or the catch, whatever happened:
async function save() {
setSaving(true);
try {
await api("/api/profile", { method: "PUT", body: profile });
} catch (err) {
setError(err.message);
} finally {
setSaving(false);
}
}
Second, the order carries an Idempotency-Key header: a random id made once for this checkout. The given server saves its answer under the key, so a retry with the same key gets the first answer back instead of starting a new checkout. React runs your component again on every render. useState(() => …) runs its function on the first render only, so the key stays the same.
The check logs in as Ana, adds the Mug and the Lamp, fills the form and clicks Pay. The first answer is lost on purpose:
> click "Pay"
screen while waiting: button: Pay (disabled)
answer: failed: the connection dropped before the answer arrived
alert: Failed to fetch button: Pay
> click "Pay"
POST /api/checkout → 201 status: Order 8 placed: $41
GET /payments → 200 [{"amount": 41, "id": "pay_1", "order_id": 8, "status": "succeeded"}]
Your task: in client/src/pages/CheckoutPage.jsx, make a key once with crypto.randomUUID() and send it as the order's Idempotency-Key header. Add a sending state and a Pay button that is disabled while sending. Set sending to true before the request and back to false in a finally.
Assignment
Follow the Coding Tutorial to make Pay safe to click twice: one key per checkout, sent with the order, and a button that is disabled while the order is on its way.
Hint
Give useState a function, not a value, so the key is made on the first render only. Put setSending(false) in finally, because after a failure the rest of the try never runs.
Free plan: 30 tutor messages. PRO is unlimited.
