<RiftEvent>
Scopes a subtree to a single event. Owns the eventId, the
lazily-loaded Stripe handle, AND the ticket-type selection state
every descendant reads. Mount inside <RiftProvider> around any
event-specific UI.
Selection lives here (not inside <AvailabilityList>) so the
picker and its reserve action can be siblings — sticky bottom CTA,
two-column picker + persistent summary, separate panels with
intermediate content. Any layout that keeps everything under one
<RiftEvent> works.
Import
import { RiftEvent } from "@feelrift/react";
Basic usage
import { EventHeader, RiftEvent, RiftProvider } from "@feelrift/react";
<RiftProvider>
<RiftEvent eventId="evt_summerfest_2026">
<EventHeader />
</RiftEvent>
</RiftProvider>;
Props
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
eventId | string | Yes | — | evt_… prefixed event ID from the Experiences API. |
children | ReactNode | Yes | — | Subtree that consumes the event scope. |
eventId
The event the surrounded UI displays and acts on. Validated
server-side on every request; the SDK doesn't validate the prefix
client-side, so a typo surfaces as a 404 on the first config call.
Render output
Renders no DOM wrapper of its own. Provides two contexts to the subtree:
RiftEventContext—eventIdand the Stripe Promise.AvailabilitySelectionContext— the selection map and its helpers (setQuantity,clearSelection,toReservationItems). Consumed viauseAvailabilitySelection().
Behavior
- Event-scoped selection. Selection state is a
ReadonlyMap<ticketTypeId, quantity>plus the three helpers above. Any descendant —<TicketRow>,<ReserveButton>, custom carts, programmatic reset buttons — callsuseAvailabilitySelection()to read or mutate. Layout depth doesn't matter; siblings, deep nests, slot-composed wrappers all see the same scope. - Per-event isolation. Mount two
<RiftEvent>instances on the same page with differenteventIds and each gets its own isolated selection. Mutating one doesn't affect the other. - Selection is ephemeral. Lives in React state, not the
persisted store. Reservations supersede it — once
<ReserveButton>succeeds, the local map is no longer authoritative and consumers typically callclearSelection()in the success callback. - Eager Stripe.js load. When
useTicketingConfig()populates the cache andticketingConfig.stripeis set, this component dynamically imports@stripe/stripe-js, initializes it with the returned publishable key plus{ stripeAccount: ticketingConfig.stripe.connectedAccountId }, and stashes the resolved Promise in React state.<CheckoutForm>consumes it via context so Stripe Elements mounts without a separate loader call. - Free or disabled Ticketing skips the import. When
ticketingConfig.stripe === null,loadStripeis never called and the optional Stripe peers don't need to be installed. - Stripe peer missing. If
loadStripe()throws because the peer isn't installed, the component logs a warning to the console and continues — free-event embeds keep working; paid embeds need the peers (see Installation → Paid events). - Promise is component-local. The Stripe
Promiselives in React state, not the persisted store, because it has no JSON-serializable representation and never needs to survive a reload — Stripe.js re-downloads on each session anyway. - Multiple instances allowed. Mounting more than one
<RiftEvent>in the same<RiftProvider>scopes each subtree to its own event ID. Reservation, checkout, and config state are per-event, so this works for multi-event pickers without collision.
Examples
Two events side-by-side
<RiftProvider>
<section>
<RiftEvent eventId="evt_summerfest_2026">
<EventHeader />
<AvailabilityList />
</RiftEvent>
</section>
<section>
<RiftEvent eventId="evt_winterfest_2026">
<EventHeader />
<AvailabilityList />
</RiftEvent>
</section>
</RiftProvider>
Each section maintains its own reservation and checkout state.
Dynamic event ID from a router
function EventPage({ params }: { params: { eventId: string } }) {
return (
<RiftEvent eventId={params.eventId}>
<EventHeader />
<AvailabilityList />
<ReserveButton requireCaptcha />
<CheckoutForm returnUrl="https://your.site/checkout/return" />
</RiftEvent>
);
}
Two-panel layout with a sticky reserve action
Picker and reserve action live in distinct visual surfaces. Both
read from the same <RiftEvent> selection scope — no extra
wrapping needed.
<RiftEvent eventId="evt_summerfest_2026">
<main className="scrollable-tickets">
<AvailabilityList />
</main>
<footer className="sticky-cta">
<ReservationSummary />
<ReserveButton requireCaptcha />
</footer>
</RiftEvent>
Custom selection consumer alongside the picker
A "selection so far" badge that updates as the user picks tickets,
sitting outside <AvailabilityList>:
import { useAvailabilitySelection } from "@feelrift/react";
function SelectionBadge() {
const { selection } = useAvailabilitySelection();
const total = Array.from(selection.values()).reduce((a, b) => a + b, 0);
return total > 0 ? <span>{total} selected</span> : null;
}
<RiftEvent eventId="evt_summerfest_2026">
<header>
<h2>Pick your tickets</h2>
<SelectionBadge />
</header>
<AvailabilityList />
<ReserveButton requireCaptcha />
</RiftEvent>;
Related
RiftProvider— must wrap<RiftEvent>.CheckoutForm— consumes the Stripe Promise this component manages.- Installation → Paid events — Stripe peer requirements.