<ReserveButton>
Submits POST /events/{eventId}/ticketing/reservations with the
current selection. Reads selection from the event-scoped context
provided by <RiftEvent>, so the button can sit anywhere under it —
sibling to <AvailabilityList>, in a sticky footer, in a sidebar,
nested inside a custom panel. Disabled when Ticketing is disabled, the
selection is empty, a request is in flight, or — with requireCaptcha
— until the captcha widget is ready. Surfaces server errors inline
beneath the button.
Import
import { ReserveButton } from "@feelrift/react";
Basic usage
import {
AvailabilityList,
CaptchaWidget,
ReserveButton,
RiftEvent,
RiftProvider,
} from "@feelrift/react";
<RiftProvider>
<RiftEvent eventId="evt_summerfest_2026">
<AvailabilityList />
<CaptchaWidget />
<ReserveButton requireCaptcha />
</RiftEvent>
</RiftProvider>;
Props
Accepts all ButtonHTMLAttributes<HTMLButtonElement> (except
onClick — managed internally) plus:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
asChild | boolean | No | false | Swap the root element via Radix Slot. |
label | ReactNode | No | t("reservation.reserve") | Text inside the button. |
loadingLabel | ReactNode | No | t("reservation.reserving") | Label shown while the request is in flight. |
onReserveSuccess | () => void | No | — | Fires after the reservation succeeds. |
onReserveError | (error: RiftApiError) => void | No | — | Fires after a failed reservation. Receives the typed error. |
requireCaptcha | boolean | No | false | Disable the button until useCaptcha().isReady === true. Set to true whenever a <CaptchaWidget> is mounted nearby. |
className | string | No | — | Appended to the SDK's rift-reserve-button class. |
disabled | boolean | No | false | Forwarded to the button. Combined with internal disable conditions. |
…rest | ButtonHTMLAttributes<HTMLButtonElement> | No | — | Forwarded to the root element. |
label / loadingLabel
One-off overrides per button instance. For app-wide changes, pass
strings={{ "reservation.reserve": "…" }} to <RiftProvider>
instead — keeps localization in one place.
requireCaptcha
Pairs with <CaptchaWidget>. When true:
- The button is
disableduntiluseCaptcha().isReadyflips. - After the reservation request, the captcha solution is automatically consumed and the widget re-arms.
Without requireCaptcha, the button submits regardless of captcha
state — useful for embeds that use a different gating strategy or
mount their own captcha logic.
onReserveSuccess / onReserveError
Per-button hooks for the same events <RiftProvider on> exposes
globally. Use the per-button form when you need to navigate or
update state in the immediate parent; use the provider-level form
for cross-cutting concerns (observability, analytics).
Render output
Renders a <button class="rift-reserve-button"> (or the slot
root). When a RiftApiError is in local state, also renders a
sibling <div class="rift-reserve-button__error"> with a
locale-aware message:
rate_limited→t("error.rateLimited", { seconds })insufficient_inventory→t("error.insufficientInventory")- everything else →
t("error.generic")
For full UI control over the error, attach onReserveError and
hide the inline error via CSS (.rift-reserve-button__error { display: none; })
or by replacing the component with useReservation() directly.
Behavior
- Request payload. Reads the selection from the surrounding
<AvailabilityList>viauseAvailabilitySelection()and the cached availability viauseAvailability(). Builds the request estimate withcomputeEstimate(items, waves)so the server can echo it back for client-side reconciliation. - Captcha consumed automatically. The SDK attaches the
captcha solution as the
altcha-payloadheader on the outbound request. After the request resolves (or fails with analtcha_payload_*error), the solution is consumed and the widget re-arms. - Disabled conditions (OR'd together):
- Selection is empty.
- A reservation request is in flight (
useReservation().isLoading). requireCaptcha && !useCaptcha().isReady.- The consumer passed
disabled.
- Non-Rift throw wrapping. Network errors and transport bugs
are wrapped in
new RiftApiError({ code: "unknown", status: 0, … })soonReserveErrorandon.erroralways receive one shape. - Error broadcast. All errors also fire
<RiftProvider on={{ error }}>.
Errors
Errors surfaced via onReserveError. All are RiftApiError
instances. See Error handling
for the narrowing pattern and the errors reference
for typed extensions per code.
| Status | Code | When | Recovery |
|---|---|---|---|
400 | validation_error | Items or estimate failed Zod validation server-side. | Surface field errors from extensions.issues. |
403 | altcha_payload_missing | Captcha solution missing or expired. | Mount <CaptchaWidget>; widget auto-refreshes. |
403 | altcha_payload_invalid | Captcha solution failed verification. | Widget auto-refreshes; retry. |
404 | event_not_found | eventId doesn't resolve to a known event or has no waves yet. | Confirm the ID and that the event has live waves. |
409 | insufficient_inventory | Requested tickets aren't available — sold out, or no wave is currently selling them. | Refetch availability; show "sold out" / "not on sale yet". |
409 | invalid_reservation | The reservation was refused (sold out, race, etc.). | Refetch availability; restart selection. |
422 | max_per_order_exceeded | One or more ticket types exceed their per-order cap. Carries violations[]. | Show each cap; reduce the offending rows. |
429 | rate_limited | Per-identity rate limit exceeded; extensions.retryAfterSeconds. | Disable and show countdown. |
Examples
Standard usage with captcha
<AvailabilityList />
<CaptchaWidget />
<ReserveButton requireCaptcha />
Custom label + navigation on success
<ReserveButton
label="Hold my tickets"
loadingLabel="Holding…"
requireCaptcha
onReserveSuccess={() => router.push("/checkout")}
/>
Slot the root for full visual control
<ReserveButton asChild requireCaptcha>
<button className="my-cta">Reserve →</button>
</ReserveButton>
The Slot preserves the click handler, disabled state, and ARIA attributes; only the visual element changes.
Typed error handling
import { isRiftApiError, ReserveButton } from "@feelrift/react";
<ReserveButton
requireCaptcha
onReserveError={(err) => {
if (isRiftApiError(err, "max_per_order_exceeded")) {
const [first] = err.extensions.violations;
if (first)
toast(`Max ${first.maxPerOrder} per order for ${first.ticketTypeId}.`);
} else if (isRiftApiError(err, "insufficient_inventory")) {
toast("These tickets just sold out.");
refetchAvailability();
}
}}
/>;
Related
<AvailabilityList>— provides the selection context this button consumes.<CaptchaWidget>— pairs withrequireCaptcha.useReservation— underlying hook for custom button implementations. Public types exported from@feelrift/react.- Error handling.