Options

WebSocketOptions<In, Out> configures a connection. Pass it to createWebSocket(url, options) or useWebSocket(url, options).

createWebSocket<In, Out>('wss://api.example.com/live', {
  protocols: ['v2.json'],
  json: true,
  schema: Message,
  reconnect: { maxAttempts: 10, shouldReconnect: close => close.code !== 4001 },
  heartbeat: { interval: 25_000, pong: 'pong', timeout: 5_000 },
  queue: { limit: 500 },
  logger: console.debug
});

Summary

OptionTypeDefault
protocolsstring | string[]none
binaryType'blob' | 'arraybuffer''blob'
jsonbooleanfalse
parse(data: unknown) => unknownnone
serialize(message: Out) => SendDatanone
schemaStandardSchemaV1<unknown, In>none
reconnectboolean | ReconnectOptionstrue
heartbeatHeartbeatOptionsnone
queueboolean | QueueOptionstrue
logger(message: string, ...details: unknown[]) => voidnone
WebSockettypeof WebSocketglobal WebSocket (browser only)

Incoming data goes through parse (or json), then schema, before it reaches lastMessage and onMessage. Outgoing messages go through serialize (or json).

Connection

protocols

Sub-protocol(s) passed to the WebSocket constructor. With useWebSocket(url), connections are shared per URL and protocols.

{ protocols: ['graphql-transport-ws'] }

binaryType

How binary messages are exposed: 'blob' (the browser default) or 'arraybuffer'.

{ binaryType: 'arraybuffer', parse: data => new Uint8Array(data as ArrayBuffer) }

WebSocket

The WebSocket implementation to use. In browsers it defaults to the global WebSocket. During a server render (when there is no window), the client never connects unless you pass an implementation here.

Use it for tests (a mock) or for Node (the ws package):

import WebSocket from 'ws';
createWebSocket(url, { WebSocket: WebSocket as unknown as typeof globalThis.WebSocket });

In development, pass a class from createChaosWebSocket to add latency, lost messages and dropped connections. See Chaos testing.

Messages

json

JSON.parse incoming messages and JSON.stringify outgoing ones.

Incoming data that isn't valid JSON is not delivered. It is reported to onError as a MessageError with the original data.

parse

A custom decoder for incoming event.data. It takes precedence over json. If it throws, the message is reported to onError as a MessageError (with the thrown error as cause) and not delivered.

{ parse: data => decodeMsgPack(data as ArrayBuffer) }

serialize

A custom encoder for outgoing messages. It takes precedence over json. It must return something WebSocket.send accepts: a string, Blob, ArrayBuffer or typed array.

{ serialize: (message: Out) => encodeMsgPack(message) }

schema

Validate incoming messages, after parsing, with any Standard Schema library: zod 3.24+, valibot 1.0+, arktype 2.0+ and others. Invalid messages are reported to onError as a MessageError with issues and are not delivered. The In type is inferred from the schema.

import { z } from 'zod';

const Tick = z.object({ symbol: z.string(), price: z.number() });
const ticks = createWebSocket('wss://api.example.com/ticks', { json: true, schema: Tick });
// ticks.getSnapshot().lastMessage: { symbol: string; price: number } | undefined

Async schemas are supported. Messages are still delivered in the order they arrived.

Reliability

reconnect

Reconnect with exponential backoff after an unexpected close or a failed attempt. false disables reconnecting. An object tunes it:

FieldTypeDefault
maxAttemptsnumberInfinityMaximum consecutive reconnect attempts. When the last one fails, the client gives up: the status becomes 'closed' and ready rejects
minDelaynumber1000Delay before the first retry, in ms
maxDelaynumber30000Upper bound for the delay, in ms
factornumber2Backoff multiplier
jitternumber1Randomly shave up to this fraction (0–1) off each delay
minUptimenumber5000How long a connection must stay open before the backoff resets, in ms. One that closes sooner counts as a failed attempt. 0 resets on every open
shouldReconnect(close: CloseInfo) => booleanretryableCloseReturn false to stop reconnecting after this close. The default retries every close except 1002, 1003 and 1008
retryAfter(close: CloseInfo) => number | undefinedretryAfterHow long the server asked to wait before the next attempt, in ms, or undefined. The default reads a 1012 or 1013 close whose reason is a number of seconds

The delay before attempt n is min(maxDelay, minDelay Ɨ factor^(nāˆ’1)), minus up to jitter of it at random. With the defaults, each delay is picked at random between 0 and 1 s, 2 s, 4 s, 8 s, … up to 30 s.

When retryAfter returns a delay, the client waits that long plus the usual delay. A 1013 (Try Again Later) close that names no delay starts the backoff at maxDelay instead. See Overloaded servers.

While a retry is scheduled, the client also retries immediately when the browser fires online or the page becomes visible again, unless the server asked it to wait.

A shouldReconnect of your own replaces the default. Call the exported retryableClose from it to keep the default rule:

import { createWebSocket, retryableClose } from 'react-websocket';

createWebSocket(url, {
  reconnect: {
    maxAttempts: 8,
    // 4001: our server's "invalid token" close code
    shouldReconnect: close => close.code !== 4001 && retryableClose(close)
  }
});

See Reconnecting and heartbeats.

heartbeat

Send a ping on an interval while open. Use it to keep connections alive through proxies that close idle connections, and, with timeout, to detect connections that died without a close event.

FieldTypeDefault
intervalnumberrequiredSend message every interval ms while open
messagestring'ping'Sent as-is, not passed through serialize or json
pongstringnoneIncoming data equal to this is swallowed instead of delivered
timeoutnumbernoneIf nothing arrives within this many ms after a ping, close with code 4000 and reconnect
{ heartbeat: { interval: 25_000, message: 'ping', pong: 'pong', timeout: 5_000 } }

Any incoming message counts as a sign of life, not just pong.

A ping is also sent right away when the browser fires online or the page becomes visible again, unless one is already waiting for an answer. A connection that died while the laptop slept is then found within timeout, instead of after up to interval more.

queue

Buffer send() calls made while not connected, and send them in order when the socket opens.

ValueBehavior
true (default)Queue at most 100 messages, like { limit: 100 }
{ limit: n }Queue at most n messages. When full, the oldest is dropped (and logged), skipping priority messages unless nothing else is queued. Infinity keeps every message
{ maxAge: ms }Drop a queued message that has waited longer than ms instead of sending it, calling its onDrop. Checked when the socket opens and whenever another message is queued. A message's own maxAge overrides it. Default Infinity
falseDon't queue. send() while not open drops the message and returns false

To decide per message instead, pass SendOptions to send(): { volatile: true } drops one message, { key } keeps only the newest queued message per key, and { priority: true } queues a message ahead of the others, and { maxAge } drops one that waited too long.

Queued messages are sent right after open listeners run, so a handshake sent from onOpen goes out first. See First-message auth. The queue is cleared by close() and when the client gives up reconnecting.

{ queue: { limit: 500, maxAge: 60_000 } }

See Expiring stale messages.

Debugging

logger

Receives debug messages about connection attempts, retries, heartbeat timeouts and dropped messages.

{ logger: (message, ...details) => console.debug('[ws]', message, ...details) }

Hook-only options

useWebSocket(url, options) also accepts onOpen, onMessage, onClose, onError and enabled. See Handlers.