esc

Type to search...

Migrate from react-use-websocket

react-use-websocket is a thin hook over the browser WebSocket. It works for small surfaces but you build everything else yourself: type safety, subscription ref counting, in-flight tracking, optimistic updates, structured debug. This page is a practical conversion guide, not a feature comparison.

See also vs react-use-websocket for the side-by-side trade-offs.

API mapping

Prop Type Default Description
useWebSocket(url, options) → new WebSocketManager + hooks Construct the manager once at module scope; consume it via hooks where needed
sendJsonMessage(msg) → useSocketSend().send(msg) Typed against TClientMsg
lastJsonMessage → useSocketEvent(manager, type, handler) Push handlers per discriminator value, not pull from a state slot
readyState → useSocketConnectionState(manager) Returns "idle" | "disconnected" | "connecting" | "connected" | "reconnecting"
shouldReconnect / reconnectAttempts / reconnectInterval → reconnectMaxAttempts / reconnectBaseDelayMs / reconnectMaxDelayMs Exponential backoff with jitter is built in
onOpen / onClose / onError → onReady config callback or useSocketConnectionState hook Reacting to a successful (re)connect is what onReady is for; for state transitions use useSocketConnectionState in React or manager.addConnectionStateListener outside
filter → deserialize + useSocketEvent narrowing Decide what to keep at deserialize boundary; narrow with discriminator
share / heartbeat → module-level manager + ping / isPong config Sharing is automatic; ping is opt-in via the ping callback

Before: react-use-websocket

Chat.tsx (before)
import useWebSocket, { ReadyState } from "react-use-websocket";

function Chat() {
  const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(
    "wss://api.example.com/ws",
    {
      shouldReconnect: () => true,
      reconnectAttempts: 5,
      reconnectInterval: (attempt) => Math.min(1000 * 2 ** attempt, 5000),
    },
  );

  useEffect(() => {
    if (lastJsonMessage?.type === "chat") {
      appendMessage(lastJsonMessage);
    }
  }, [lastJsonMessage]);

  return (
    <button
      disabled={readyState !== ReadyState.OPEN}
      onClick={() => sendJsonMessage({ type: "chat", text: "hi" })}
    >
      Send
    </button>
  );
}

After: react-socket

Chat.tsx (after)
import {
  WebSocketManager,
  useSocketConnectionState,
  useSocketEvent,
  useSocketSend,
} from "@luciodale/react-socket";

type TClientMsg = { type: "chat"; text: string };
type TServerMsg = { type: "chat"; text: string };

const manager = new WebSocketManager<TClientMsg, TServerMsg>({
  url: "wss://api.example.com/ws",
  serialize: (m) => JSON.stringify(m),
  deserialize: (r) => JSON.parse(r),
  reconnectMaxAttempts: 10,
  reconnectBaseDelayMs: 1_000,
  reconnectMaxDelayMs: 30_000,
});

function Chat() {
  const state = useSocketConnectionState(manager);
  const { send } = useSocketSend(manager);

  useSocketEvent(manager, "chat", (msg) => {
    appendMessage(msg);
  });

  return (
    <button
      disabled={state !== "connected"}
      onClick={() => send({ type: "chat", text: "hi" })}
    >
      Send
    </button>
  );
}

Subscriptions: built in, not built by you

A common workaround in react-use-websocket apps is a context provider that ref counts subscriptions over a shared socket. useSocketSubscription does this natively:

PriceBadge.tsx
// react-use-websocket: each component opens its own WebSocket if you call
// the hook with the same URL. Sharing requires a context provider you build
// yourself.

// react-socket: define the manager once at module scope. N components can
// useSocketSubscription on the same key; the library ref counts and only
// sends one subscribe to the server.
useSocketSubscription(manager, {
  key: "room:" + roomId,
  subscribe: { type: "subscribe", roomId },
  unsubscribe: { type: "unsubscribe", roomId },
});

Step by step

  1. Define TClientMsg / TServerMsg discriminated unions for everything you currently send and receive.
  2. Create one WebSocketManager at module scope, pass serialize / deserialize wrapping your current parser.
  3. Move every useEffect watching lastJsonMessage into a useSocketEvent per discriminator value.
  4. Replace sendJsonMessage calls with useSocketSend.
  5. Replace readyState branches with useSocketConnectionState; map your old ReadyState constants to the new string literals.
  6. Move reconnect tuning into the manager config; delete your custom shouldReconnect closures.
  7. If you had a homegrown subscription provider, drop it. Use useSocketSubscription with a key per logical stream.
  8. If you tracked acks manually, define getAckId in the manager config; the in-flight registry clears automatically.

Gotchas

  • react-use-websocket's lastJsonMessage is one slot. If two messages of different types land in the same render, one slot only holds the latest. useSocketEvent never drops messages.
  • Re-renders. The original triggers a re-render on every received message via lastJsonMessage. useSocketEvent only re-renders if your handler updates state.
  • Auth. The original gives you no help; consult Authentication for first-message and URL-based options.
  • If you used options.share across components, you no longer need it. The module-level manager is the share.

Next steps