esc

Type to search...

Configuration

The manager is configured through a single options object. Three fields are required: URL and a pair of serialization functions. Everything else has sensible defaults. Every option is typed against your client and server message generics.

Options at a glance

Prop Type Default Description
url * string | (() => string | Promise<string>) WebSocket endpoint URL. Pass a function to resolve it per connect (e.g. auth tokens)
serialize * (msg: TClientMsg) => string Convert outgoing messages to strings
deserialize * (raw: string) => TServerMsg Parse incoming strings into typed messages
discriminator string "type" Key used to narrow TServerMsg in useSocketEvent
getAckId (msg: TServerMsg) => string | undefined Return the ack id when an incoming message confirms an outbound send. Library auto-clears the matching in-flight entry
getSubscriptionResolvedKey (msg: TServerMsg) => string | undefined Return the subscription key when an incoming message confirms a subscribe. Library auto-clears the pending marker
ping () => TClientMsg Function returning the ping message, called each interval
isPong (msg: TServerMsg) => boolean Return true if an incoming message is a pong
pingIntervalMs number 30000 Milliseconds between pings
pongTimeoutMs number 10000 Milliseconds to wait for a pong before disconnecting
pauseHeartbeatWhenHidden boolean true Pause ping/pong while document.hidden and resume on visibility. Avoids spurious pong timeouts from background-tab throttling on mobile. Pass false to keep heartbeating in background tabs. Requires ping/isPong to take effect
reconnectMaxAttempts number 10 Maximum reconnection attempts. Pass Number.POSITIVE_INFINITY to retry forever
reconnectBaseDelayMs number 1000 Initial reconnection delay
reconnectMaxDelayMs number 30000 Maximum reconnection delay (cap on the exponential backoff)
transport IWebSocketTransport Custom WebSocket transport for testing or non-browser environments
binaryType "blob" | "arraybuffer" Tell the transport which binary type to deliver. Required when sending or receiving binary frames; widen the manager's TWire / TIncoming generics accordingly
onReady (restoredKeys: string[]) => void Fires after every (re)connect once subscriptions are restored. Best place for first-message auth and refetch logic that must run before any component mounts
onDebug (event: TDebugEvent) => void Fires for every internal lifecycle event. Best place for production telemetry that should always run

Required options

manager.ts
const manager = new WebSocketManager<TClientMsg, TServerMsg>({
  url: "wss://api.example.com/ws",
  serialize: (msg) => JSON.stringify(msg),
  deserialize: (raw) => JSON.parse(raw),
});

serialize and deserialize are typed against your generics, so TypeScript catches shape mismatches. Swap JSON for MessagePack, Protobuf, or anything else without touching the rest of your code.

Discriminator

useSocketEvent narrows server messages by one field. The default is "type". Override by passing the key as the third generic plus the discriminator option:

discriminator.ts
type TServerMsg =
  | { kind: "notification"; title: string }
  | { kind: "user-typing"; userId: string };

const manager = new WebSocketManager<TClientMsg, TServerMsg, "kind">({
  url: "wss://...",
  discriminator: "kind",
  serialize: (msg) => JSON.stringify(msg),
  deserialize: (raw) => JSON.parse(raw),
});

useSocketEvent(manager, "notification", (msg) => {
  msg.title; // narrowed
});

Lifecycle extractors

Two extractors let the library auto wire ack and subscription resolution. When either returns a string, the matching internal bookkeeping fires before your listeners run. You never call ackInFlight or resolvePendingSubscription from user code.

extractors.ts
{
  // When the server confirms an outbound send, return its ack id.
  // Library removes the matching in-flight entry automatically.
  getAckId: (msg) => (msg.type === "delivered" ? msg.ackId : undefined),

  // When the server confirms a subscribe, return the subscription key.
  // Library clears the pending marker automatically —
  // useSocketPendingSubscription flips to false.
  getSubscriptionResolvedKey: (msg) =>
    msg.type === "subscribe-ack" ? msg.channel : undefined,
}

Ping / pong

Keep alive pings prevent idle connections from being killed by proxies or load balancers. ping is a function so you can include a fresh timestamp each tick.

ping.ts
{
  ping: () => ({ type: "ping", timestamp: Date.now() }),
  isPong: (msg) => msg.type === "pong",
  pingIntervalMs: 30_000,
  pongTimeoutMs: 10_000,
}

Reconnection

Exponential backoff with jitter, capped. See the Reconnection guide.

reconnection.ts
{
  reconnectMaxAttempts: 10,
  reconnectBaseDelayMs: 1_000,
  reconnectMaxDelayMs: 30_000,
}

Lifecycle: config vs listener

Two lifecycle events are exposed as construction-time callbacks because they need to fire before any component mounts:

config-callbacks.ts
new WebSocketManager<TClientMsg, TServerMsg>({
  // ...

  // Fires after every (re)connect, once subscriptions are restored.
  // Best place for first-message auth (no React mount needed).
  async onReady(restoredKeys) {
    const token = await auth.getValidAccessToken();
    manager.send({ data: { type: "auth", token } });
  },

  // Fires for every internal event — feed monitoring, audit logs, etc.
  onDebug(event) {
    if (event.type === "deserialize-error") report(event.error);
  },
});

Everything else uses an addXxxListener method on the manager (or its React hook wrapper). Listeners return an unsubscribe so teardown is automatic in React effects.

lifecycle.ts
// Imperative — outside React, or in a top-level bridge.
const stop1 = manager.addSendIntentListener(({ data, ackId }) => { /* ... */ });
const stop2 = manager.addInFlightDropListener((messages) => { /* ... */ });
const stop3 = manager.addLastUnsubscribeListener((key, data) => { /* ... */ });
const stop4 = manager.addConnectionStateListener(() => {
  const state = manager.getConnectionState();
});

// React — colocate with the component that owns the domain.
useSocketSendIntent(manager, ({ data, ackId }) => { /* ... */ });
useSocketInFlightDrop(manager, (messages) => { /* ... */ });
useSocketLastUnsubscribe(manager, (key, data) => { /* ... */ });
const state = useSocketConnectionState(manager);

Custom transport

Default transport is the browser's native WebSocket. Implement IWebSocketTransport for tests or non browser environments.

custom-transport.ts
import type { IWebSocketTransport } from "@luciodale/react-socket";

const customTransport: IWebSocketTransport = {
  connect(url, protocols) { /* ... */ },
  disconnect(code, reason) { /* ... */ },
  send(data) { /* ... */ },
  readyState: 0,
  onopen: null,
  onclose: null,
  onmessage: null,
  onerror: null,
};

new WebSocketManager<TClientMsg, TServerMsg>({
  url: "wss://...",
  serialize: (msg) => JSON.stringify(msg),
  deserialize: (raw) => JSON.parse(raw),
  transport: customTransport,
});

Next

  • API Reference — hooks and manager surface
  • Patterns — scenario driven examples
  • Testing — MockTransport, simulating lifecycle, hook tests