Two modes
Every outbound message is sent via
useSocketSend (inside React) or
manager.send (outside React). You pick per
call: fire and forget, or tracked with an
ackId.
Fire and forget
Send and move on. Good for cursor positions, typing pings, anything where a lost message does not matter because the next one is already in flight.
const { send } = useSocketSend(manager);
send({ type: "cursor_move", x: 100, y: 200 });Acknowledged send
Pass an ackId as the second argument and the
library tracks the message as in flight. The server must echo back a
confirmation that carries the same id. Wire the
getAckId extractor in manager config and the
library clears the tracking automatically — you never call
ackInFlight from user code.
// manager config
{
getAckId: (msg) => (msg.type === "delivered" ? msg.ackId : undefined),
}function ChatInput({ roomId }: { roomId: string }) {
const { send } = useSocketSend(manager);
function onSend(text: string) {
const id = crypto.randomUUID();
send({ type: "message", id, roomId, text }, id);
}
return <button onClick={() => onSend("hi")}>send</button>;
}Optimistic UI
Three hooks cover the full lifecycle:
useSocketSendIntent fires on every
send(), even offline — insert the
optimistic record here. useSocketEvent on
the delivered message flips it to "sent."
useSocketInFlightDrop fires when the
connection drops before ack — mark as failed. Mount them together
in one bridge component.
const manager = new WebSocketManager<TClientMsg, TServerMsg>({
url: "wss://...",
serialize: JSON.stringify,
deserialize: (raw) => JSON.parse(raw),
getAckId: (msg) => (msg.type === "delivered" ? msg.ackId : undefined),
});function ChatOptimisticBridge() {
useSocketSendIntent(manager, ({ data, ackId }) => {
if (data.type === "message" && ackId) {
useChatStore.getState().insertOptimistic(data, ackId);
}
});
useSocketEvent(manager, "delivered", (msg) => {
useChatStore.getState().markDelivered(msg.ackId);
});
useSocketInFlightDrop(manager, (messages) => {
for (const { id } of messages) {
useChatStore.getState().markFailed(id);
}
});
return null;
}Disabled UI when offline
Compose useSocketSend with
useSocketConnectionState. They are kept
separate on purpose so components that only need
send do not re render on every state
transition.
function SendButton() {
const state = useSocketConnectionState(manager);
const { send } = useSocketSend(manager);
const canSend = state === "connected";
return (
<button
disabled={!canSend}
onClick={() => send({ type: "ping" })}
>
ping
</button>
);
}Persist undelivered
For messages that must survive a page refresh, pair this flow with createUndeliveredSync. Store on send, remove on ack, retry on reconnect.
Next
- Patterns — acknowledged send walkthrough
- API Reference