Start one named operation
When the user submits, generate or obtain one operation identity and store it with client state. Transition from idle to pending, disable or replace only the control that would create the same effect, and show what is being processed. React's useState stores component state across renders; a setter schedules another render, so the operation rule should live in explicit state.
The client guard improves the interface. It does not authorize the user or make two network requests impossible. The trusted service must validate the request, enforce permissions, and reconcile the operation identity.
Keep unknown distinct from failure
| State | User decision | Client behavior |
|---|---|---|
| Pending | Wait while the request is in flight. | Keep the ID and block a duplicate submit. |
| Succeeded | Continue with the confirmed result. | Clear duplicate risk for this operation. |
| Confirmed failure | Correct or retry under the service contract. | Allow a deliberate new attempt. |
| Outcome unknown | Check status or escalate before retrying. | Preserve the ID and block a blind resend until the same ID is reconciled as succeeded or failed. |
Stripe's idempotency documentation is one API example where a supported write request can use a key to return the recorded result on a retry. Define your target API's identity, retention, and conflict behavior with its owner; do not assume Stripe behavior elsewhere.
Announce status without taking focus
Keep the submitted data and next decision visible. Render a stable role="status" aria-atomic="true" container before an ordinary pending or completed update, rather than mounting the live region only when text changes. W3C's ARIA22 technique describes this polite pattern and advises explicit atomic context where needed. An unknown outcome needs visible text plus a keyboard-reachable status or escalation action.
Test the state contract outside JSX
The local evidence/P98/operation-state.mjs model preserves the first identity, ignores an old completion, blocks a second request while pending or unknown, reconciles an unknown outcome only for that same identity, and permits a new attempt after confirmed failure:
npm test --prefix sites/reactjsx.com/evidence/P98
It is synchronous. The JSX adapter, browser behavior, server authorization, and server-side idempotency still need application-level tests.
Verification checklist
- One operation identity exists before the write starts.
- Pending and unknown outcomes both prevent a blind duplicate request.
- A completion updates only the identity it belongs to.
- An unknown outcome stays blocked until the service reconciles that same identity as succeeded or failed.
- Confirmed failure and unknown outcome have different messages and actions.
- The backend validates, authorizes, and reconciles the operation identity.
Is disabling the submit button enough?
No. It can reduce one accidental repeat in one rendered view. It cannot cover refresh, timeout, retry, concurrent clients, or server behavior.