websocket-event
v0.0.5
Published
Typed event-based WebSocket client for browsers
Readme
websocket-event
Typed event-based WebSocket client for browsers
The core client has zero runtime dependencies
import { WebSocketClient } from "websocket-event";
// React
import { useWebSocket, createWebSocketContext } from "websocket-event/react";Installation
npm install websocket-eventFor React integrations, install React 19 or newer:
npm install react@19Message format
By default, incoming and outgoing messages use this structure:
type Message = {
key: string;
data: unknown;
};Example payload:
{
"key": "message.created",
"data": {
"id": "42",
"text": "Hello"
}
}For full type safety, define a union containing every supported message:
type AppMessage =
| {
key: "message.created";
data: {
id: string;
text: string;
};
}
| {
key: "user.online";
data: {
userId: string;
};
}
| {
key: "typing.changed";
data: {
userId: string;
typing: boolean;
};
};The message key and its corresponding data type are then inferred by invoke() and event listeners.
Core client
Creating and connecting a client
import { WebSocketClient } from "websocket-event";
const client = new WebSocketClient<AppMessage>("wss://example.com/ws", {
autoReconnect: true,
reconnectDelay: 2_000,
onOpen: () => {
console.log("Connected");
},
onClose: (event) => {
console.log("Disconnected", event.code, event.reason);
},
onError: (error) => {
console.error("WebSocket error", error);
},
});
client.connect();The constructor does not connect automatically. Call connect() when the client should open the connection.
Listening for events
const unsubscribe = client.on("message.created", (message, event) => {
console.log(message.data.id);
console.log(message.data.text);
console.log(event.origin);
});
// Remove this listener later.
unsubscribe();You can also remove listeners explicitly:
const handleUserOnline = (message: Extract<AppMessage, { key: "user.online" }>) => {
console.log(message.data.userId);
};
client.on("user.online", handleUserOnline);
client.off("user.online", handleUserOnline);
// Remove every listener for this key.
client.off("user.online");Sending typed events
Use invoke() to serialize an event as JSON:
client.invoke("typing.changed", {
userId: "user-1",
typing: true,
});Both the key and data are checked by TypeScript:
client.invoke("typing.changed", {
userId: "user-1",
typing: "yes", // TypeScript error
});Use send() when you need to send raw WebSocket-compatible data:
client.send("plain text");
client.send(new Uint8Array([1, 2, 3]));Both send() and invoke() return false when the socket is not open and true after the data has been passed to WebSocket.send().
Connection state
const unsubscribe = client.subscribeState((state) => {
console.log(state);
});Possible states:
type ConnectionState =
| "initializing"
| "connecting"
| "open"
| "closed"
| "reconnecting";The current state and native socket are also available directly:
console.log(client.connectionState);
console.log(client.rawSocket);Reconnecting and closing
client.reconnect();
client.close(1000, "Finished");Calling close() disables automatic reconnect for that closure.
When the client is no longer needed, destroy it to close the socket and clear all listeners:
client.destroy();Updating options
client.updateOptions({
autoReconnect: true,
reconnectDelay: 5_000,
});Updating options does not recreate the current socket. Protocol changes take effect when a new connection is created.
Custom message parser
The default parser accepts stringified JSON or an already parsed value and validates that it contains string key and data fields.
Use parseMessage for another server format:
const client = new WebSocketClient<AppMessage>("wss://example.com/ws", {
parseMessage(event) {
const payload = JSON.parse(String(event.data)) as {
event: AppMessage["key"];
payload: unknown;
};
return {
key: payload.event,
data: payload.payload,
} as AppMessage;
},
});Parser errors are passed to onError.
React hook
Import React-specific APIs from the /react entry point:
import { useWebSocket } from "websocket-event/react";
const options = {
autoReconnect: true,
reconnectDelay: 2_000,
}
function Chat() {
const {
connectionState,
invoke,
on,
reconnect,
close,
} = useWebSocket<AppMessage>("wss://example.com/ws", options);
function sendTyping() {
invoke("typing.changed", {
userId: "user-1",
typing: true,
});
}
return (
<section>
<p>State: {connectionState}</p>
<button onClick={sendTyping}>Send typing event</button>
<button onClick={reconnect}>Reconnect</button>
<button onClick={() => close()}>Close</button>
</section>
);
}Subscribe to events inside an effect:
import { useEffect } from "react";
import { useWebSocket } from "websocket-event/react";
function Notifications() {
const { on } = useWebSocket<AppMessage>("wss://example.com/ws", {
autoReconnect: true,
});
useEffect(() => {
return on("message.created", (message) => {
console.log(message.data.text);
});
}, [on]);
return null;
}The hook creates a new client when url changes and destroys the previous client during cleanup.
React context
Use createWebSocketContext() when multiple components should share one connection.
Create a typed context once, outside React components:
import { createWebSocketContext } from "websocket-event/react";
export const WebSocket = createWebSocketContext<AppMessage>();Add the provider near the application root:
import { WebSocket } from "./WebSocket";
import type { PropsWithChildren } from "react";
const options = {
autoReconnect: true,
reconnectDelay: 2_000,
}
export function WebSocketProvider({ children }: PropsWithChildren) {
return (
<WebSocket.Provider
url="wss://example.com/ws"
options={options}
>
{children}
</WebSocket.Provider>
);
}Reading the shared client
import { WebSocket } from "./WebSocket";
function SendButton() {
const { invoke, connectionState } = WebSocket.useWebSocket();
return (
<button
disabled={connectionState !== "open"}
onClick={() => {
invoke("typing.changed", {
userId: "user-1",
typing: true,
});
}}
>
Send
</button>
);
}Listening for an event
import { WebSocket } from "./WebSocket";
function MessageListener() {
WebSocket.useWebSocketEvent("message.created", (message) => {
console.log(message.data.id, message.data.text);
});
return null;
}useWebSocketEvent() keeps the subscription stable while using the latest callback implementation.
Reading connection state
import { WebSocket } from "./WebSocket";
function ConnectionIndicator() {
const state = WebSocket.useConnectionState();
return <span>{state}</span>;
}Controlling the connection
The provider accepts connectEnabled:
<WebSocket.Provider
url="wss://example.com/ws"
connectEnabled={isAuthenticated}
>
{children}
</WebSocket.Provider>When connectEnabled becomes false, the client closes the current connection. When it becomes true, the client connects again.
Options
type WebSocketClientOptions<M> = {
autoReconnect?: boolean;
reconnectDelay?: number;
protocols?: string | string[];
parseMessage?: (event: MessageEvent<unknown>) => M;
onOpen?: (event: Event) => void;
onMessage?: (message: M, event: MessageEvent) => void;
onClose?: (event: CloseEvent) => void;
onError?: (error: unknown) => void;
};| Option | Default | Description |
| --- | --- | --- |
| autoReconnect | false | Reconnect after an unexpected closure. |
| reconnectDelay | 1000 | Delay before reconnecting, in milliseconds. |
| protocols | undefined | WebSocket subprotocol or list of subprotocols. |
| parseMessage | Built-in parser | Converts an incoming MessageEvent into a typed message. |
| onOpen | — | Called after the connection opens. |
| onMessage | — | Called for every successfully parsed message. |
| onClose | — | Called after the connection closes. |
| onError | — | Called for socket errors and parsing errors. |
Type utilities
The root entry point also exports utility types:
import type {
Message,
MessageKey,
MessageByKey,
MessageData,
ConnectionState,
WebSocketClientOptions,
WebSocketEventCallback,
} from "websocket-event";Example:
type CreatedMessage = MessageByKey<AppMessage, "message.created">;
type CreatedData = MessageData<AppMessage, "message.created">;
type AppMessageKey = MessageKey<AppMessage>;Browser support
The package uses the native browser WebSocket API. A compatible global WebSocket implementation must exist in the runtime.
