Get started with React
A book list, stock counts that follow the server as they change, and a Buy button that handles a sold-out book. The app talks to the bookshop server from any of the other guides on port 4000; the TypeScript one is the quickest to run beside it. The finished app is examples/react.
1. Create the app
npm create vite@latest bookshop-web -- --template react-ts
cd bookshop-web
npm install @rayfold/client @rayfold/react2. Send API calls to the server
In development the page comes from Vite on port 5173 and the API from port 4000. Let Vite pass /rayfold on:
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react()],
// the app calls /rayfold on the dev server, which passes it on to src/server.ts
server: { proxy: { "/rayfold": "http://localhost:4000" } },
});The browser still names the page's origin on every command, and a Rayfold server refuses commands from origins it does not know, which is what stops other websites from acting for your users. On the server, allow Vite's origin:
const endpoint = createHttpHandler(server, {
viewer: (req) => viewerFrom(req.headers.authorization),
// the page comes from the Vite dev server, another origin, so the browser names that origin on every command
allowedOrigins: ["http://localhost:5173"],
});3. Provide the client
const client = new RayfoldClient({
transport: createFetchTransport({
url: "/rayfold",
headers: () => ({ authorization: "Bearer customer" }),
}),
});
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RayfoldProvider client={client}>
<App />
</RayfoldProvider>
</StrictMode>,
);The token stands in for your real sign-in. The server turns it into the viewer that the schema's @allow rules check.
4. List the books
function BookList() {
const { data, error, loading } = useQuery<{ items: Book[] }>("books", { page: { first: 20 } }, { shape: "{ items { id title author { name } } }" });
if (error) return <p role="alert">Could not load the books.</p>;
if (loading && !data) return <p>Loading...</p>;
return (
<ul>
{data?.items.map((book) => (
<li key={book.id}>
<strong>{book.title}</strong> by {book.author.name} <Stock id={book.id} /> <BuyButton id={book.id} />
</li>
))}
</ul>
);
}useQuery sends the query and keeps the result in the client cache. The shape names exactly the fields the list shows, and the authors of the whole page are loaded in one call on the server.
5. Follow the stock
// Follows the stock as it changes, whoever buys or restocks.
function Stock({ id }: { id: string }) {
const { data } = useLive<{ stock: number }>("book", { id }, { shape: "{ id stock }" });
return <span data-stock={id}>{data ? `${data.stock} in stock` : "..."}</span>;
}useLive keeps the query open. When anyone buys or restocks, the server sends a patch and only this component renders again. Open the app in two tabs and buy in one of them to watch the other change.
6. Buy a copy
function BuyButton({ id }: { id: string }) {
const [buy, { running, error }] = useCommand<Book>("buy");
const soldOut = (error as RayfoldClientError | undefined)?.is("OutOfStock");
return (
<>
<button disabled={running} onClick={() => buy({ bookId: id })}>
Buy
</button>
{soldOut && <span role="alert"> Sold out</span>}
</>
);
}useCommand gives each purchase its own idempotency key, so a retried request never buys twice. The book the command returns updates the cache, and every component showing that book re-renders. OutOfStock is declared in the schema, so the component checks for it by name.
7. Run it
With the server running on port 4000:
npm run devOpen http://localhost:5173.
Next
- More on
useQuery,useCommand,useLiveand server rendering: the React guide. - Show a purchase before the server answers, and queue it while offline: Offline and optimistic.
- How live queries work: Live updates.