esc

Type to search...

Subscriptions

Subscriptions tell the server "I want updates on this stream." The library ref counts internally so ten components can all mount a useSocketSubscription for the same key and only one message hits the server. When the last consumer unmounts, the unsubscribe fires automatically. After a reconnect, every active subscription is restored transparently.

The hook

useSocketSubscription owns the lifecycle. The key is the ref counted identifier. The subscribe and unsubscribe payloads are typed against TClientMsg.

basic.tsx
useSocketSubscription(manager, {
  key: "chat:general",
  subscribe: { type: "subscribe", channel: "general" },
  unsubscribe: { type: "unsubscribe", channel: "general" },
});

Bookkeeping-only subscribe (no payload)

You can call useSocketSubscription with no subscribe / unsubscribe payload at all. The ref count still bumps and useSocketLastUnsubscribe still fires when it reaches zero, but nothing goes on the wire. useSocketPendingSubscription stays false for those keys (there is no server ack to wait for).

Ref counting in action

Render the same hook twice in one tree. Only one subscribe hits the server; only one unsubscribe fires when both unmount.

PriceBadge.tsx
function PriceBadge({ ticker }: { ticker: string }) {
  useSocketSubscription(manager, {
    key: `price:${ticker}`,
    subscribe: { type: "subscribe", channel: ticker },
    unsubscribe: { type: "unsubscribe", channel: ticker },
  });
  const price = usePriceStore((s) => s.prices[ticker]);
  return <span>{price}</span>;
}

// Renders N copies → 1 subscribe on the wire, N readers on the store.
<List>
  {tickers.map((t) => <PriceBadge key={t} ticker={t} />)}
</List>

Reading the stream

useSocketSubscription manages the server side stream. Reading is a separate concern. For streams consumed by many components, pair with a single useSocketEvent bridge that writes into a zustand store. See the rendered state from a stream pattern.

PriceBridge.tsx
// One bridge, shared by all consumers
function PriceBridge() {
  useSocketEvent(manager, "price", (msg) => {
    usePriceStore.getState().set(msg.ticker, msg.value);
  });
  return null;
}

Joining state

Most servers ack a subscribe with a confirmation message. Wire getSubscriptionResolvedKey in the manager config so the library clears the pending marker automatically, then read it with useSocketPendingSubscription.

manager.ts
// manager config
{
  getSubscriptionResolvedKey: (msg) =>
    msg.type === "subscribe-ack" ? msg.channel : undefined,
}
Room.tsx
function Room({ id }: { id: string }) {
  useSocketSubscription(manager, {
    key: id,
    subscribe: { type: "subscribe", channel: id },
    unsubscribe: { type: "unsubscribe", channel: id },
  });

  const joining = useSocketPendingSubscription(manager, id);
  if (joining) return <span>joining...</span>;
  return <MessageList roomId={id} />;
}

Restoration on reconnect

Subscribe only, no read

Sometimes a component needs the server to keep a stream alive but does not render from it (presence keepalive, typing indicator host). That is fine — subscribing and reading are independent.

PresenceKeepalive.tsx
function PresenceKeepalive() {
  useSocketSubscription(manager, {
    key: "presence",
    subscribe: { type: "sub-presence" },
    unsubscribe: { type: "unsub-presence" },
  });
  return null;
}

Next