esc

Type to search...

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

  1. Decide the wire protocol. Pick a discriminator field (commonly "type"). List every client and server message variant.
  2. 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.
  3. Build TClientMsg / TServerMsg unions; convert socket.on(event, fn) calls into useSocketEvent.
  4. 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.
  5. Translate ack callbacks. Add getAckId to the config and a delivered server variant. Replace each emit(..., cb) with a paired send + useSocketEvent("delivered").
  6. Replace room joins with useSocketSubscription. The component owns the lifecycle; the library ref counts.
  7. 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.
  8. Wire useSocketInFlightDrop to your retry / failed UI. Socket.IO's volatile vs reliable distinction maps to whether you set ackId.

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