Recipe 01@openseat/react · @openseat/adapter-rest
Booking map with live inventory and holds
useInventory keeps statuses fresh through the adapter (polling by default, server-sent events here). useHold runs the hold lifecycle with a countdown that follows your server's expiry.
- Rules (maxSeats, preventOrphans) are checked in the browser for a smooth experience and again on your server.
- categoryPrices is display text only: the SDK never computes prices.
import { useState } from "react";
import type { LayoutDocument } from "@openseat/core";
import { createRestAdapter } from "@openseat/adapter-rest";
import { BookingMap, useHold, useInventory } from "@openseat/react";
// Create the adapter once (module scope or useState), never inline in render.
const api = createRestAdapter({
baseUrl: `${location.origin}/api`,
credentials: "same-origin",
realtime: { kind: "sse", url: (id) => `${location.origin}/api/v1/events/${encodeURIComponent(id)}/stream` },
});
export function Tickets({ layout, eventId }: { layout: LayoutDocument; eventId: string }) {
const { statuses } = useInventory(api, eventId); // live over server-sent events
const hold = useHold(api, eventId, { maxTickets: 6, layout });
const [selection, setSelection] = useState<readonly string[]>([]);
return (
<>
<BookingMap
layout={layout}
statuses={statuses}
selectedSeatIds={selection}
onSelectionChange={(next) => setSelection(next)}
rules={{ maxSeats: 6, preventOrphans: true }}
categoryPrices={{ vip: "₹1,499", stalls: "₹499", balcony: "₹299" }}
/>
<button disabled={!hold.canHold || selection.length === 0} onClick={() => hold.hold(selection)}>
Hold {selection.length} seats
</button>
{hold.state.status === "held" ? <p>Held for {Math.ceil(hold.remainingMs / 1000)} s</p> : null}
</>
);
}