esc

Type to search...

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.

fire-and-forget.tsx
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.ts
// manager config
{
  getAckId: (msg) => (msg.type === "delivered" ? msg.ackId : undefined),
}
ChatInput.tsx
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.

manager.ts
const manager = new WebSocketManager<TClientMsg, TServerMsg>({
  url: "wss://...",
  serialize: JSON.stringify,
  deserialize: (raw) => JSON.parse(raw),
  getAckId: (msg) => (msg.type === "delivered" ? msg.ackId : undefined),
});
ChatOptimisticBridge.tsx
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.

SendButton.tsx
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