Skip to main content

<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:

NameTypeRequiredDefaultDescription
asChildbooleanNofalseSwap the root element via Radix Slot.
labelReactNodeNot("reservation.reserve")Text inside the button.
loadingLabelReactNodeNot("reservation.reserving")Label shown while the request is in flight.
onReserveSuccess() => voidNoFires after the reservation succeeds.
onReserveError(error: RiftApiError) => voidNoFires after a failed reservation. Receives the typed error.
requireCaptchabooleanNofalseDisable the button until useCaptcha().isReady === true. Set to true whenever a <CaptchaWidget> is mounted nearby.
classNamestringNoAppended to the SDK's rift-reserve-button class.
disabledbooleanNofalseForwarded to the button. Combined with internal disable conditions.
…restButtonHTMLAttributes<HTMLButtonElement>NoForwarded 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 disabled until useCaptcha().isReady flips.
  • 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_limitedt("error.rateLimited", { seconds })
  • insufficient_inventoryt("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> via useAvailabilitySelection() and the cached availability via useAvailability(). Builds the request estimate with computeEstimate(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-payload header on the outbound request. After the request resolves (or fails with an altcha_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, … }) so onReserveError and on.error always 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.

StatusCodeWhenRecovery
400validation_errorItems or estimate failed Zod validation server-side.Surface field errors from extensions.issues.
403altcha_payload_missingCaptcha solution missing or expired.Mount <CaptchaWidget>; widget auto-refreshes.
403altcha_payload_invalidCaptcha solution failed verification.Widget auto-refreshes; retry.
404event_not_foundeventId doesn't resolve to a known event or has no waves yet.Confirm the ID and that the event has live waves.
409insufficient_inventoryRequested tickets aren't available — sold out, or no wave is currently selling them.Refetch availability; show "sold out" / "not on sale yet".
409invalid_reservationThe reservation was refused (sold out, race, etc.).Refetch availability; restart selection.
422max_per_order_exceededOne or more ticket types exceed their per-order cap. Carries violations[].Show each cap; reduce the offending rows.
429rate_limitedPer-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();
}
}}
/>;
  • <AvailabilityList> — provides the selection context this button consumes.
  • <CaptchaWidget> — pairs with requireCaptcha.
  • useReservation — underlying hook for custom button implementations. Public types exported from @feelrift/react.
  • Error handling.