Migrate from socket.io-client
Socket.IO is more than a transport. It bundles its own protocol on top
of WebSocket, plus rooms, namespaces, and ack callbacks. react-socket
is a thin manager around the standard WebSocket
API. Migrating means picking up the protocol shape your server already
speaks (or settling on one) and dropping the framework abstractions.
See also vs Socket.IO for the side-by-side trade-offs.
Concept mapping
| Prop | Type | Default | Description |
|---|---|---|---|
io(url, options.auth) | → new WebSocketManager + onReady auth | — | Send auth as the first message via onReady; see Authentication guide |
socket.on(event, fn) | → useSocketEvent(manager, type, fn) | — | Discriminator narrows the message type automatically |
socket.emit(event, payload) | → useSocketSend().send({ type: event, ...payload }) | — | Events are encoded as the discriminator field on a typed message |
socket.emit(event, payload, ack) | → send + useSocketEvent on the delivered type | — | Pass ackId on send; getAckId resolves the in-flight entry; delivered handler runs UI updates |
Rooms (server-side) | → subscribe / unsubscribe messages | — | react-socket has no rooms. The server still groups; the client just sends a typed subscribe payload |
io.connect / disconnect | → manager.connect() / manager.disconnect() | — | Lifecycle is explicit |
Reconnect: built in | → also built in | — | reconnectMaxAttempts / Base / Max delays; same exponential backoff with jitter |
Acks: ack callback | → ackId + getAckId + delivered event | — | Receive-side handler instead of send-side closure; in-flight registry handles drops |
Namespaces | → multiple WebSocketManager instances | — | If you really need isolation, see Multiple connections |
binary: enabled by default | → binaryType + TWire generic | — | Opt-in; widen the wire type, set binaryType. See Binary frames |
Before: socket.io-client
chat.ts (before)
import { io } from "socket.io-client";
const socket = io("https://api.example.com", {
auth: { token: getAccessToken() },
});
socket.on("connect", () => {
socket.emit("join", "room:42");
});
socket.on("chat", (msg) => {
appendMessage(msg);
});
function send(text: string) {
socket.emit("chat", { text }, (ack) => {
if (ack.ok) markDelivered(ack.id);
});
}After: react-socket
ChatRoom.tsx (after)
import {
WebSocketManager,
useSocketEvent,
useSocketSend,
useSocketSubscription,
} from "@luciodale/react-socket";
type TClientMsg =
| { type: "auth"; token: string }
| { type: "join"; room: string }
| { type: "leave"; room: string }
| { type: "chat"; id: string; text: string };
type TServerMsg =
| { type: "auth-ok"; userId: string }
| { type: "chat"; id: string; text: string }
| { type: "delivered"; ackId: string };
const manager = new WebSocketManager<TClientMsg, TServerMsg>({
url: "wss://api.example.com/ws",
serialize: (m) => JSON.stringify(m),
deserialize: (r) => JSON.parse(r),
getAckId: (m) => (m.type === "delivered" ? m.ackId : undefined),
onReady() {
manager.send({ data: { type: "auth", token: getAccessToken() } });
},
});
function ChatRoom({ room }: { room: string }) {
const { send } = useSocketSend(manager);
useSocketSubscription(manager, {
key: room,
subscribe: { type: "join", room },
unsubscribe: { type: "leave", room },
});
useSocketEvent(manager, "chat", (msg) => appendMessage(msg));
return (
<button
onClick={() =>
send({ type: "chat", id: crypto.randomUUID(), text: "hi" }, "ack-id")
}
>
Send
</button>
);
}Acks: callback to delivered event
ack.ts
// socket.io: emit takes a callback, server invokes it on ack.
socket.emit("chat", payload, (ack) => {
if (ack.ok) markDelivered(ack.id);
});
// react-socket: pass an ackId on send, define getAckId in the config.
// The library tracks in-flight automatically; the callback sits at the
// receive boundary instead of the send boundary.
manager.send({ data: payload, ackId: "ack-1" });
useSocketEvent(manager, "delivered", (msg) => {
markDelivered(msg.ackId);
});
// On disconnect, anything still in flight is reported once via
// useSocketInFlightDrop, so you can mark it failed in one place.Rooms become subscriptions
Socket.IO's rooms are a server-side concept. The client only knows it
emitted a join. react-socket pushes the same workflow into the
protocol: send a subscribe payload, listen
for the typed chat or update message. The library ref counts so N
components can subscribe to the same room without N joins.
rooms.ts
// socket.io rooms are server-side groupings the server pushes to.
// react-socket has no notion of rooms; the server pushes whatever it wants
// to the connected client. The client tells the server which streams it
// cares about via subscriptions, ref counted on the client.
// If your server already does join/leave by name, the only client change
// is mapping io's emit("join", room) and on(room, fn) into one
// useSocketSubscription with subscribe / unsubscribe payloads and a
// useSocketEvent listening for the typed chat / update / etc messages.Step by step
- Decide the wire protocol. Pick a discriminator field (commonly
"type"). List every client and server message variant. - If your server still runs Socket.IO, expose a plain WS endpoint that speaks your new protocol. Migrate one feature at a time behind a feature flag.
- Build
TClientMsg/TServerMsgunions; convertsocket.on(event, fn)calls intouseSocketEvent. - Move auth out of
io(url, { auth }). Pick a strategy from the Authentication guide; first-message is the closest to Socket.IO's auth handshake. - Translate ack callbacks. Add
getAckIdto the config and adeliveredserver variant. Replace eachemit(..., cb)with a pairedsend + useSocketEvent("delivered"). - Replace room joins with
useSocketSubscription. The component owns the lifecycle; the library ref counts. - If you used multiple namespaces, decide whether you really need multiple managers. Most cases collapse into one. If you do need separate sockets (different lifecycles, different auth), instantiate two managers and wire them independently — there is nothing in the library that pins you to one.
- Wire
useSocketInFlightDropto your retry / failed UI. Socket.IO's volatile vs reliable distinction maps to whether you setackId.
What you give up
- Long-polling fallback. WebSocket is the only transport.
- Server-side rooms / broadcast helpers. You implement those on the server with whatever WS library you use.
- Built-in binary message types as a first-class concept. react-socket supports binary, but you opt in via the wire type generic.
- Magic ack callbacks. You get the same semantics, but the wiring is explicit.
What you gain
- Fully typed protocol. The compiler catches missing variants.
- Smaller client bundle (the manager is a few kilobytes; Socket.IO ships its own protocol code).
- Subscription ref counting on the client.
- Pluggable transport for tests via
MockTransport. - Inspector for development debugging without touching network panel internals.
Next steps
- Getting started — quick start
- Authentication — first-message and URL-based auth
- Sending Messages — ack tracking and optimistic UI