React quickstart
Connect a React application to a room with presence, durable storage, and a secure token endpoint.
This guide adds a shared counter and collaborator presence to a Next.js application. The same provider and hook setup works in Vite, Remix, or another client-rendered React application.
Here is what you are building. Both windows below run the same components you are about to write:
Install the SDKs
pnpm add @polynomialhq/react @polynomialhq/server@polynomialhq/react runs in the browser. @polynomialhq/server uses node:crypto and must only
run in trusted server code.
Add your credentials
Create a project and secret key in the PolynomialHQ dashboard, then add them to the server environment:
POLYNOMIAL_PROJECT_ID=project_your_id
POLYNOMIAL_SECRET_KEY=sk_your_secret_keyNever expose the secret key through a NEXT_PUBLIC_ variable or send it to the browser. Anyone
holding it can mint a token for any room in the project.
Create an authentication endpoint
The client posts { room } to this endpoint. Your application should authenticate the current
user before issuing a token.
import { PolynomialServer } from "@polynomialhq/server";
import { NextResponse } from "next/server";
const polynomial = new PolynomialServer({
secretKey: process.env.POLYNOMIAL_SECRET_KEY!,
});
export async function POST(request: Request) {
// Replace this with the user from your auth provider.
const user = {
id: "user_123",
name: "Ada",
};
const { room } = (await request.json()) as { room: string };
const token = polynomial.authorizeRoom({
projectId: process.env.POLYNOMIAL_PROJECT_ID!,
role: "write",
roomId: room,
userId: user.id,
userInfo: {
name: user.name,
},
});
return NextResponse.json({ token });
}authorizeRoom() signs the token locally and returns a string; it does not call the API, so there
is nothing to await.
The endpoint must verify that the signed-in user is allowed to enter the requested room. See Authentication for a production authorization pattern.
Create the client
Create the client once, outside React rendering, and pass it to PolynomialProvider.
"use client";
import { createClient, PolynomialProvider } from "@polynomialhq/react";
import type { ReactNode } from "react";
const client = createClient({
authEndpoint: "/api/polynomial-auth",
});
export function CollaborationProvider({ children }: { children: ReactNode }) {
return <PolynomialProvider client={client}>{children}</PolynomialProvider>;
}Always pass a client. PolynomialProvider will build a default one if you omit the prop, but a
default client has no authEndpoint or publicApiKey, so entering a room by id throws
createClient requires authEndpoint, publicApiKey, or an enterRoom token.
Add the provider near the root of the part of your application that uses collaboration:
import { CollaborationProvider } from "./collaboration-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<CollaborationProvider>{children}</CollaborationProvider>
</body>
</html>
);
}Enter a room
Declare the presence, storage, and event types used by your room.
"use client";
import { RoomProvider } from "@polynomialhq/react";
import type { ReactNode } from "react";
export type Presence = {
name: string;
online: boolean;
};
export type Storage = {
count: number;
};
export type Events = {
celebrated: {
emoji: string;
};
};
export function CounterRoom({ children }: { children: ReactNode }) {
return (
<RoomProvider<Presence, Storage, Events>
id="counter:demo"
initialPresence={{ name: "Ada", online: true }}
initialStorage={{ count: 0 }}
>
{children}
</RoomProvider>
);
}The room connects when RoomProvider mounts and leaves when it unmounts. Each unique id
identifies a separate collaboration boundary.
Read and update shared state
"use client";
import { useMutation, useOthers, useRoomStatus, useStorage } from "@polynomialhq/react";
import type { Events, Presence, Storage } from "./counter-room";
export function SharedCounter() {
const count = useStorage<Presence, Storage, Events, number>((storage) => storage.count);
const others = useOthers<Presence>();
const status = useRoomStatus();
const increment = useMutation<Presence, Storage>(({ storage }) => {
storage.updateAt<number>(["count"], (current = 0) => current + 1);
}, []);
return (
<section>
<p>{status === "joined" ? "Live" : "Connecting…"}</p>
<strong>{count}</strong>
<button type="button" onClick={increment}>
Add one
</button>
<p>{others.length + 1} people here</p>
</section>
);
}The four type parameters on useStorage are presence, storage, broadcast events, and the type your
selector returns. Reuse the same types you gave RoomProvider.
Render the component inside the room:
import { CounterRoom } from "./counter-room";
import { SharedCounter } from "./shared-counter";
export default function Page() {
return (
<CounterRoom>
<SharedCounter />
</CounterRoom>
);
}Verify it
Open the page in two browser windows. Both windows should show the presence count, and
incrementing the counter in one window should update the other. Reload one window: the count
should come back from durable storage rather than resetting to the initialStorage seed.
Next steps
- Model cursor or selection state with Presence.
- Structure durable data and mutations with Storage.
- Send reactions and one-off signals with Broadcasts.
- Add undo and redo with History.