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
- Define
TClientMsg/TServerMsgdiscriminated unions for everything you currently send and receive. - Create one
WebSocketManagerat module scope, passserialize/deserializewrapping your current parser. - Move every
useEffectwatchinglastJsonMessageinto auseSocketEventper discriminator value. - Replace
sendJsonMessagecalls withuseSocketSend. - Replace
readyStatebranches withuseSocketConnectionState; map your oldReadyStateconstants to the new string literals. - Move reconnect tuning into the manager config; delete your custom
shouldReconnectclosures. - If you had a homegrown subscription provider, drop it. Use
useSocketSubscriptionwith a key per logical stream. - If you tracked acks manually, define
getAckIdin the manager config; the in-flight registry clears automatically.
Gotchas
react-use-websocket'slastJsonMessageis one slot. If two messages of different types land in the same render, one slot only holds the latest.useSocketEventnever drops messages.- Re-renders. The original triggers a re-render on every received message via
lastJsonMessage.useSocketEventonly 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.shareacross components, you no longer need it. The module-level manager is the share.
Next steps
- Getting started — quick start in five minutes
- Authentication — first-message auth pattern
- Testing — MockTransport for unit tests