A TypeScript-first WebSocket manager for React that handles the socket plumbing you don't want to build.
Documentation · NPM · GitHub
Coming from react-use-websocket or a raw useEffect(() => new WebSocket(...)), these are the things you stop writing by hand:
- Typed message schemas. Client and server union types flow through
send,serialize,deserialize, and every callback. Noany, no casts. - Ref counted subscriptions. Five components can subscribe to the same channel. One subscribe message hits the server. The unsubscribe fires on the last unmount.
- In-flight tracking. Tag a send with an
ackId. Know when the server confirms, or when the connection drops before delivery. - Offline message queue. Sends made while disconnected persist to storage and flush on reconnect.
- Reconnection with backoff. Exponential backoff with jitter, subscriptions restore themselves.
- DevTools inspector. A drop in component that shows traffic, subscription ref counts, and in-flight state in real time.
Built for streaming LLM clients, realtime trading UIs, chat, presence, and agentic workflows.
Full comparisons: vs react-use-websocket (thin hook camp) · vs Socket.IO (same tier, different trade offs)
- React 16.8+ (hooks).
- TypeScript 4.7+ recommended for full generic inference.
- Modern evergreen browsers. Tested on Chrome 90+, Firefox 88+, Safari 14+, Edge 90+.
npm install @luciodale/react-socketThe minimum to get a typed WebSocket connection with automatic reconnection.
// app.tsx
import { WebSocketManager, useConnectionState } from "@luciodale/react-socket"
type ClientMsg = { type: "echo"; text: string }
type ServerMsg = { type: "echo_reply"; text: string }
const manager = new WebSocketManager<ClientMsg, ServerMsg>({
url: "wss://your-server.com/ws",
serialize: (msg) => JSON.stringify(msg),
deserialize: (raw) => JSON.parse(raw),
onMessageReceived: (msg) => {
console.log("received:", msg)
},
})
manager.connect()
function App() {
const state = useConnectionState(manager)
return (
<div>
<p>Connection: {state}</p>
<button
onClick={() =>
manager.send({ data: { type: "echo", text: "hello" } })
}
>
Send
</button>
</div>
)
}Change a field in ClientMsg or ServerMsg and TypeScript tells you every place that needs updating. The generic types flow through serialize, deserialize, onMessageReceived, and send with zero casts.
- Automatic reconnection — exponential backoff with jitter, configurable max attempts and delays. Subscriptions restore themselves on reconnect without extra code.
- Ref-counted subscriptions — multiple components can subscribe to the same channel. The manager tracks reference counts internally and only sends the subscribe/unsubscribe message when the first component mounts or the last one unmounts.
- In-flight tracking — tag outgoing messages with an
ackId. The manager holds them until the server acknowledges or the connection drops, then notifies you of unacknowledged messages so nothing gets silently lost. - Keep-alive (ping/pong) — configurable ping interval and pong timeout. If the server goes silent, the manager detects it and triggers reconnection before your users notice.
- Undelivered sync — optional persistent storage for messages that failed to send. Survives page reloads via localStorage (or any custom
IStorageimplementation). - Pluggable transport — the default uses the browser
WebSocketAPI, but you can swap in any transport that implementsIWebSocketTransport. Useful for testing or non-browser environments. - Full TypeScript generics —
WebSocketManager<TClientMsg, TServerMsg>propagates your message types across the entire API surface. Discriminated unions, generics, compile-time safety. - DevTools inspector — a drop-in
InspectorPanelcomponent that visualizes connection state, message flow, subscription ref counts, and in-flight messages in real time.
Multiple components subscribing to the same key share a single server subscription. The manager deduplicates automatically.
// chat-room.tsx
function useChatRoom(roomId: string) {
useEffect(() => {
manager.subscribe(`room:${roomId}`, {
type: "join_room",
roomId,
})
return () => {
manager.unsubscribe(`room:${roomId}`, {
type: "leave_room",
roomId,
})
}
}, [roomId])
}If three components call subscribe("room:lobby"), the join message is sent once. When all three unmount, the leave message is sent once.
Tag a message with ackId and the manager holds it until you confirm delivery or the connection drops.
// send-with-ack.tsx
const id = crypto.randomUUID()
manager.send({
data: { type: "place_order", item: "espresso" },
ackId: id,
})
// When the server confirms:
manager.ackInFlight(id)
// If the connection drops before ack, onInFlightDrop fires
// with the list of unacknowledged messages.A built-in devtools panel for debugging WebSocket traffic. Ships as a separate export so it tree-shakes out of production builds.
// debug.tsx
import { InspectorPanel } from "@luciodale/react-socket/inspector"
function DevTools() {
return <InspectorPanel manager={manager} />
}Full documentation, configuration reference, and live examples at koolcodez.com/projects/react-socket.
MIT