Realtime
WebSocket events, presence & ACK
usePubSub is the client half of the pub/sub feature. It opens a single WebSocket to the events endpoint, subscribes to the topics you name, and streams server events into an h-state store you read reactively — connection status and the event log live there. (Presence is not a distinct store slice: it arrives as ordinary events on the 'user-presence' topic, read via events / getEventsByTopic('user-presence').)
It is resilient by default: a heartbeat (an application-level ping/pong JSON message, not a protocol PING frame) keeps the socket warm, drops trigger exponential-backoff reconnection, and every delivered message is acknowledged back to the server so the ACK/redelivery guarantees on the backend actually hold.
Configure it on the backend through the pubsub config block; consume it on the frontend with this one hook. No manual socket lifecycle, no reconnection loops to write.
Hook configuration#
usePubSub(config) takes the identity and topics to subscribe to, plus optional resilience tuning. userId is required client-side — it scopes the connection and, in unauthenticated deployments, is enforced by the maxClientsPerUser guard on the server. When the backend authenticates the WebSocket handshake (config.authenticate, wired whenever an access-token secret is set), the identity is taken from the verified handshake token instead and this query param is ignored.
userIdstringRequiredThe authenticated user the socket belongs to. Always sent as a query param. In deployments without server-side auth it identifies the connection for per-user client limits and targeted delivery; when the backend authenticates the WebSocket handshake (config.authenticate), identity is derived from the verified token's subject and this query param is ignored.
topicsstring[]OptionalTopics to subscribe to on connect. Changing this array re-subscribes automatically. '*' receives everything you're permitted to see.
Example: ["orders", "notifications:user-42"]
["*"]wsUrlstringOptionalOverride the WebSocket origin. When omitted the hook derives ws://wss:// from window.location, so same-origin deployments need no URL at all.
wsPathstringOptionalThe subscribe path — must match the pubsub.wsPath configured on the backend.
"/api/events/subscribe"autoReconnectbooleanOptionalReconnect automatically after an unexpected close. An auth-rejection close (code 4001) is respected and does not retry.
truemaxReconnectAttemptsnumberOptionalHow many backoff attempts before giving up and surfacing an error on the store.
10reconnectBaseDelaynumberOptionalBase delay in ms for exponential backoff (base · 2^attempt), capped by reconnectMaxDelay.
1000reconnectMaxDelaynumberOptionalUpper bound in ms on any single reconnect delay.
30000heartbeatIntervalnumberOptionalHow often (ms) to send an application-level ping message (JSON { type: 'ping' }, answered by a 'pong' — not a WebSocket PING control frame) so proxies and the server keep the socket alive.
30000debugbooleanOptionalLog connection lifecycle and message handling to the console.
falseWhat the hook returns#
Everything you need to render realtime UI: live connection flags, the typed event list, presence/identity and imperative controls. Reads are reactive — when a new event arrives, components re-render.
1const { isConnected, events, subscribe } = usePubSub({2 userId: user.id,3 topics: ["orders"],4});5 6// ACK is automatic — every event with a messageId is acknowledged7return (8 <section>9 <span data-live={isConnected}>{isConnected ? "live" : "offline"}</span>10 {events.map((e) => (11 <Event key={e.id} topic={e.topic} payload={e.data} />12 ))}13 </section>14);isConnected / isConnectingbooleanOptionalConnection status flags. isConnecting covers both initial connect and reconnect attempts so you can show one 'linking…' state.
eventsPubSubEvent[]OptionalThe received events (id, topic, data, timestamp, receivedAt, messageId, isRedelivery), capped by a fixed 100-event ring buffer so memory stays bounded. (PubSubConfig still exposes a maxEvents field with a '(default 100)' JSDoc, but usePubSub never reads it — it is accepted-but-ignored, so setting it has no effect; the buffer is always 100.)
clientId / subscribedTopicsstring | string[]OptionalThe server-assigned client id and the topics the server confirms you're subscribed to.
error / reconnectAttemptError | numberOptionalThe last connection error (or null) and the current backoff attempt count for diagnostics UI.
subscribe / unsubscribe(topics: string[]) => voidOptionalAdjust subscriptions on a live socket without reconnecting. subscribe REPLACES the entire subscribed set with the array you pass (it does not merge/add), so the working way to drop a topic is to call subscribe again with the reduced array. Note: unsubscribe is currently a server-side no-op — the backend's 'unsubscribe' handler only logs the request and never removes topics, so the client keeps receiving those events; use subscribe with a smaller array to narrow instead. The server caps how many topics one client may hold (pubsub.maxTopicsPerClient, default 64); topics beyond the cap are silently dropped (the first 64 are kept, de-duped, each clamped to 256 chars — the client is not notified), so the request still succeeds with a truncated topic set.
connect / disconnect / clearEvents / getEventsByTopicfunctionsOptionalImperative controls: force a (re)connect, tear down cleanly, empty the buffer, or read just one topic's events. Note: connect() reopens the socket only while the connection is still ref-held (e.g. the component stays mounted); once disconnect() fully releases it (refCount → 0), connect() is a no-op — you must remount or change the config to re-establish the socket.
Related sections