Keep the offer identity with the result

Store the offer ID, expiry supplied by the trusted service, pending offer ID, visible state, and message separately. When an operator accepts, submit the offer ID and wait for the service result. Accept a response only when it matches the pending offer. A late response for an earlier offer must not change the later one.

React's useReducer reference keeps related update logic in one reducer. The important contract is the business identity: the backend determines whether the named offer is still available and whether the operator may accept it.

Model the recovery states explicitly

The local model covers offered, accepting, accepted, expired, unavailable, and failed. It rejects stale responses, leaves an unknown service result unresolved, and prevents a further acceptance attempt after an expiry state:

node --test sites/reactjsx.com/evidence/P133/waitlist-offer-state.test.mjs

A small illustrative status can make the change visible without moving focus:

<p role="status">This offer expired before acceptance.</p>

W3C's status-message guidance describes programmatically determinable status updates. The markup is illustrative only; test the delivered interface with target browsers and assistive technologies.

Do not infer acceptance from intent

Show an actionable button only in the offered state. In accepting, identify which offer is being checked. In expired, unavailable, or failed, preserve enough context for the operator to understand what happened and offer a safe next step such as returning to the current list. Do not offer an automatic retry without a new service decision.

For a separate unknown mutation outcome, see Prevent Double Submit After an Unknown Operation Outcome. For a stale asynchronous result, see Ignore Stale Async Validation Results in React.

Can this component reserve a slot?

No. The trusted service must enforce candidate eligibility, offer expiry, appointment authority, and the final acceptance. The component only represents the returned state.