Connections and reconnection
Display connection state, configure retry behavior, and handle errors.
The client connects when a room is entered and can automatically retry temporary failures.
Take the network down, edit while it is offline, and bring it back:
Observe status
The native room status has seven states:
| Status | Meaning |
|---|---|
idle | No connection attempt has started |
connecting | A transport is being opened |
connected | The WebSocket is open and the room join is in progress |
joined | The room is ready |
reconnecting | A retry is scheduled or in progress |
closed | The connection was intentionally closed |
error | The connection stopped on an error |
import { useRoomStatus } from "@polynomialhq/react";
function ConnectionBadge() {
const status = useRoomStatus();
return (
<span data-status={status}>
{status === "joined" ? "Live" : status === "reconnecting" ? "Reconnecting…" : status}
</span>
);
}useStatus() collapses the same machine into five coarser values:
useRoomStatus() | useStatus() |
|---|---|
idle | initial |
connecting, connected | connecting |
joined | connected |
reconnecting | reconnecting |
closed, error | disconnected |
useConnectionSnapshot() also exposes connectionId, userId, roomId, role, and
lastError.
Configure retries
Set reconnection on the client for every room:
const client = createClient({
authEndpoint: "/api/polynomial-auth",
reconnect: {
delayMs: (attempt) => Math.min(500 * 2 ** attempt, 10_000),
enabled: true,
maxAttempts: 8,
},
});Or override it for one room:
<RoomProvider
id="document:42"
initialPresence={{ cursor: null }}
initialStorage={{ blocks: [] }}
reconnect={{
delayMs: 1_000,
enabled: true,
maxAttempts: 5,
}}
>
<Editor />
</RoomProvider>reconnect: true enables the default retry policy and reconnect: false disables it. The defaults
are enabled: true, unlimited maxAttempts, and an exponential backoff of
Math.min(1000 * 2 ** (attempt - 1), 10_000) — one second, then two, four, eight, and ten seconds
from then on. attempt starts at 1.
Retries only cover transport failures. A non-retryable protocol error such as unauthorized or
forbidden moves the room to error and stops reconnecting, because retrying the same rejected
token would not help. Mint a new token and re-enter the room.
Your authEndpoint is called again on every reconnect attempt, so an expired token is replaced
automatically as long as the endpoint keeps returning fresh ones.
Listen for connection loss
import { useLostConnectionListener } from "@polynomialhq/react";
useLostConnectionListener((event) => {
if (event === "lost") {
showToast("Connection lost. Retrying…");
}
if (event === "restored") {
showToast("Back online.");
}
if (event === "failed") {
showToast("Could not reconnect.");
}
});lost fires once when the room enters reconnecting, restored when it reaches joined again,
and failed when the room lands in error. Pass a stable callback — wrap it in useCallback if
it closes over state — because the listener is re-registered whenever its identity changes.
Listen for errors
import { useErrorListener } from "@polynomialhq/react";
useErrorListener((error) => {
reportError({
code: error.code,
message: error.message,
retryable: error.retryable,
});
});Possible protocol codes are unauthorized, forbidden, not_found, invalid_message,
rate_limited, and internal_error. Transport-level failures arrive without a code and are
always retryable.
What happens to state during a reconnect
- Others are cleared as soon as the room leaves
joined, so cursors disappear immediately rather than freezing in place. - Presence is re-sent automatically once the room rejoins.
- Storage stays readable throughout. Local mutations keep working and queue as pending Yjs updates, which are flushed after the room rejoins.
- Broadcasts sent while disconnected are lost. There is no replay buffer.
Keep the last rendered document on screen during a reconnect. Replacing the editor with a spinner throws away work the user can still see.
Lower-level snapshots
Framework-agnostic code can subscribe to the room:
const unsubscribe = room.subscribe((event) => {
if (event.type === "connection.changed") {
console.log(event.snapshot.connection);
}
if (event.type === "error") {
console.error(event.error);
}
});For transport-level integrations, use createRoomConnection() and subscribe to
RoomConnectionEvent.
Pending storage updates
The lower-level RoomConnection tracks outgoing Yjs updates until the server acknowledges them:
connection.getPendingYjsUpdateCount();
connection.subscribe((event) => {
if (event.type === "yjs.outbox.changed") {
setPendingSaveCount(event.pendingYjsUpdateCount);
}
});Use useStorageStatus() in React unless you specifically need the raw queue count.