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.
useSocketSubscription(manager, {
key: "chat:general",
subscribe: { type: "subscribe", channel: "general" },
unsubscribe: { type: "unsubscribe", channel: "general" },
});Pin the key + payloads in a custom hook (recommended)
Don't call useSocketSubscription directly from
feature components. Wrap it once per resource — same file as the
resource's protocol — and have every consumer use the wrapper. The
key, the
subscribe payload, and the
unsubscribe payload all derive from the same
input, so it is impossible for two call sites to drift apart.
// hooks/useSpaceSubscription.ts
import { useSocketSubscription } from "@luciodale/react-socket";
import { manager } from "../socket/manager";
export function useSpaceSubscription(spaceId: string | null) {
useSocketSubscription(manager, {
key: spaceId ?? "",
enabled: spaceId !== null,
subscribe: spaceId
? { type: "subscribe", channel: spaceId }
: undefined,
unsubscribe: spaceId
? { type: "unsubscribe", channel: spaceId }
: undefined,
});
}Now every consumer is a one-liner that cannot get the params wrong:
function SpaceHeader({ spaceId }: { spaceId: string }) {
useSpaceSubscription(spaceId);
// ...
}
function MessageList({ spaceId }: { spaceId: string }) {
useSpaceSubscription(spaceId);
// ...
}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.
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.
// 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 config
{
getSubscriptionResolvedKey: (msg) =>
msg.type === "subscribe-ack" ? msg.channel : undefined,
}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.
function PresenceKeepalive() {
useSocketSubscription(manager, {
key: "presence",
subscribe: { type: "sub-presence" },
unsubscribe: { type: "unsub-presence" },
});
return null;
}Next
- Patterns — history, optimistic sends, composition
- API Reference