useWebSocket
Open a WebSocket from a component, with automatic reconnects, an optional send queue and every message delivered in order.
import { useState } from 'react';
import { useWebSocket } from '@gio.js/react';
export function Chat({ room }: { room: string }) {
const [messages, setMessages] = useState<string[]>([]);
const { send, readyState } = useWebSocket(`/api/chat/${room}`, {
onMessage: (data) => setMessages((list) => [...list, String(data)]),
});
return (
<>
<p>{readyState === 1 ? 'Live' : 'Connecting...'}</p>
<ul>{messages.map((m, i) => <li key={i}>{m}</li>)}</ul>
<button onClick={() => send('hello')}>Say hello</button>
</>
);
}The server side is a wsHandler export in the route.ts for that path, here app/api/chat/[room]/route.ts. The socket needs its own URL: a folder cannot hold both a page.tsx and a route.ts. See wsHandler and WebSockets.
Reference
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
url (required) | string | - | Where to connect. A relative URL (/chat/lobby) resolves against the page, with ws: on an http: page and wss: on https:. A new value closes the old socket and opens one to the new URL. |
options | UseWebSocketOptions | {} | See below. |
Options
| Option | Type | Default | Description |
|---|---|---|---|
reconnect | boolean | ReconnectOptions | true | Reconnect after a close the hook did not ask for. false turns it off; an object tunes the backoff. |
shouldReconnect | (event: CloseEvent) => boolean | - | Decide per close whether to reconnect, replacing the default policy. Ignored when reconnect is false. |
queueWhileDisconnected | boolean | { maxMessages: number } | false | Keep send() calls made while the socket is not open and send them, in order, once it opens. true keeps up to 100. |
protocols | string | string[] | - | Subprotocols for the WebSocket constructor, read on each (re)connect. |
onMessage | (data: WebSocketData, event: MessageEvent) => void | - | Every message, in order. data is a string or an ArrayBuffer. |
onOpen | (event: Event) => void | - | Each time a connection opens, reconnects included. |
onClose | (event: CloseEvent) => void | - | Each time the connection closes, with its code and reason. |
The options are read when they are needed, so passing a new object or new callbacks on every render never reconnects.
ReconnectOptions
| Option | Type | Default | Description |
|---|---|---|---|
maxAttempts | number | Infinity | Consecutive attempts before giving up - ones that fail, and ones that open but close again within minUptimeMs. |
initialDelayMs | number | 500 | Backoff before the first retry. |
maxDelayMs | number | 30000 | The longest backoff. |
minUptimeMs | number | 5000 | How long a connection must stay open before the backoff starts over. |
Returns
| Field | Type | Default | Description |
|---|---|---|---|
send | (data: string | ArrayBufferLike | ArrayBufferView | Blob) => boolean | - | Send now, or queue it (with queueWhileDisconnected). false when the message was dropped: not open and not queued, the queue full, or the hook stopped. |
lastMessage | string | ArrayBuffer | null | - | The latest message, null before the first. React may batch two quick messages into one render: use onMessage when every one matters. |
readyState | number | - | 0 connecting, 1 open, 3 closed, as on WebSocket; -1 before a socket exists (server rendering). Compare with the numbers rather than WebSocket.OPEN in render code: Node 20 has no WebSocket global, so the server render would throw. |
reconnectAttempts | number | - | Retries since a connection last stayed open minUptimeMs; 0 while connected. |
isReconnecting | boolean | - | Waiting to retry after a drop. |
close | (code?: number, reason?: string) => void | - | Close for good (default code 1000): no reconnects until reconnect() or a new url. |
reconnect | () => void | - | Open a fresh connection now, with the backoff reset. |
The types are exported as UseWebSocketOptions, ReconnectOptions, UseWebSocketResult and WebSocketData.
Reconnecting
After an unintended close the hook waits and connects again. Retry n (from 0) waits between half and all of min(maxDelayMs, initialDelayMs × 2^n) - with the defaults about 0.25-0.5 s, then 0.5-1 s, 1-2 s and so on up to 15-30 s - so clients do not all return in the same instant after a server restart.
| Close code | Reconnects? |
|---|---|
1000 (the server ended the conversation) | No |
4000-4499 (the application refused, like an HTTP 4xx: 4401 from a wsHandler returning false, 4404 no handler) | No |
1001 (server shutdown or worker restart), 1006 (network loss), 1011, 1012, 1013 (over max_connections) | Yes |
4500-4999 (transient application errors) | Yes |
It never reconnects after close(), after unmount, or once maxAttempts is reached. The backoff starts over only when a connection stayed open for minUptimeMs: a server that accepts the upgrade and closes straight away (1013, 1011) keeps being backed off from.
Examples
Sending JSON
import { useWebSocket } from '@gio.js/react';
interface Cursor {
x: number;
y: number;
}
export function CursorSync({ onRemote }: { onRemote: (cursor: Cursor) => void }) {
const { send } = useWebSocket('/board/live', {
onMessage: (data) => {
if (typeof data === 'string') onRemote(JSON.parse(data) as Cursor);
},
});
return (
<div
className="board"
onPointerMove={(e) => send(JSON.stringify({ x: e.clientX, y: e.clientY }))}
/>
);
}A connection indicator
const { readyState, isReconnecting, reconnectAttempts, reconnect } = useWebSocket('/feed', {
reconnect: { maxAttempts: 10 },
});
if (isReconnecting) return <p>Reconnecting (attempt {reconnectAttempts})...</p>;
if (readyState === 3) return <button onClick={reconnect}>Reconnect</button>; // closedQueueing while offline
const { send } = useWebSocket('/notes/42', { queueWhileDisconnected: { maxMessages: 500 } });
// Sent at once when open; otherwise kept and sent on the next open.
const accepted = send(JSON.stringify({ op: 'insert', text: 'hi' }));
if (!accepted) showWarning('Change not sent');A custom reconnect policy
useWebSocket('/prices', {
// Also give up when the server says the market is closed (4503 would otherwise retry).
shouldReconnect: (event) => event.code !== 1000 && event.code !== 4503 && !(event.code >= 4000 && event.code < 4500),
});Good to know
- The hook does nothing during server rendering (
readyStateis-1,sendreturnsfalse); the socket opens after the component mounts in the browser. In the server-only root layout it never runs. - A message is only ever sent to the URL it was queued for: when
urlchanges (another room), whatever is still queued is dropped. - Binary frames always arrive as
ArrayBuffer(binaryTypeis fixed), never asBlob. - Cookies go with the upgrade request, and the server refuses cross-site upgrades by checking
Origin([security.websocket]). - A worker restart closes every socket with
1001, and the hook reconnects; room memberships are re-established by yourwsHandlerwhen it accepts the new connection. readyStatenever reports2(closing): it moves from1to3when the close completes.
Related
- WebSockets - the guide: handlers, auth, rooms, close codes.
wsHandler- the server side.broadcast- send to a room from any route handler.[websocket]-max_connections,ping_interval_secs.
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Reconnects by default with exponential backoff and jitter (reconnect: false for the old behavior). Added shouldReconnect, queueWhileDisconnected, onMessage, onOpen, onClose, reconnectAttempts, isReconnecting and reconnect(). |
v0.1.0-beta.1 | Introduced. |