GioJSdocs
On this page

useWebSocket

Open a WebSocket from a component, with automatic reconnects, an optional send queue and every message delivered in order.

app/chat/[room]/chat.tsx
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

ParameterTypeDefaultDescription
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.
optionsUseWebSocketOptions{}See below.

Options

OptionTypeDefaultDescription
reconnectboolean | ReconnectOptionstrueReconnect 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.
queueWhileDisconnectedboolean | { maxMessages: number }falseKeep send() calls made while the socket is not open and send them, in order, once it opens. true keeps up to 100.
protocolsstring | 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

OptionTypeDefaultDescription
maxAttemptsnumberInfinityConsecutive attempts before giving up - ones that fail, and ones that open but close again within minUptimeMs.
initialDelayMsnumber500Backoff before the first retry.
maxDelayMsnumber30000The longest backoff.
minUptimeMsnumber5000How long a connection must stay open before the backoff starts over.

Returns

FieldTypeDefaultDescription
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.
lastMessagestring | ArrayBuffer | null-The latest message, null before the first. React may batch two quick messages into one render: use onMessage when every one matters.
readyStatenumber-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.
reconnectAttemptsnumber-Retries since a connection last stayed open minUptimeMs; 0 while connected.
isReconnectingboolean-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 codeReconnects?
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

app/board/cursor-sync.tsx
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

tsx
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>; // closed

Queueing while offline

tsx
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

tsx
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 (readyState is -1, send returns false); 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 url changes (another room), whatever is still queued is dropped.
  • Binary frames always arrive as ArrayBuffer (binaryType is fixed), never as Blob.
  • 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 your wsHandler when it accepts the new connection.
  • readyState never reports 2 (closing): it moves from 1 to 3 when the close completes.

Version history

VersionChanges
v0.1.0-beta.8Reconnects 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.1Introduced.