Skip to content

Latest commit

 

History

History
193 lines (140 loc) · 7.13 KB

File metadata and controls

193 lines (140 loc) · 7.13 KB
react-socket logo

react-socket

A TypeScript-first WebSocket manager for React. One hook per concern, zero imperative glue.

Documentation  ·  NPM  ·  GitHub

npm version npm downloads bundle size license

Why react-socket

Coming from react-use-websocket or a raw useEffect(() => new WebSocket(...)), these are the things you stop writing by hand:

  • Typed message schemas. Client and server union types flow through send, every hook, every callback. Discriminated unions narrow automatically by a configurable key.
  • One primitive per concern. Ten hooks, each with a distinct job. No message switch, no pub/sub layer, no .on / .remove anywhere in user code.
  • Ref counted subscriptions. Five components can subscribe to the same channel. One subscribe message hits the server. The unsubscribe fires on the last unmount.
  • Lifecycle in the library. Ack matching and subscription resolution are declared once as extractors. You never call ackInFlight or resolvePendingSubscription.
  • Offline message queue. Sends made while disconnected can persist to storage and flush on reconnect.
  • Reconnection with backoff. Exponential backoff with jitter, subscriptions restore themselves.
  • DevTools inspector. A drop in component that shows traffic, subscription ref counts, and in-flight state in real time.

Built for streaming LLM clients, realtime trading UIs, chat, presence, and agentic workflows.

Full comparisons: vs react-use-websocket (thin hook camp) · vs Socket.IO (same tier, different trade offs)

Requirements

  • React 16.8+ (hooks).
  • TypeScript 4.7+ recommended for full generic inference.
  • Modern evergreen browsers. Tested on Chrome 90+, Firefox 88+, Safari 14+, Edge 90+.

Install

npm install @luciodale/react-socket

Quick start

One manager at module level. One hook to react to incoming events. One hook to send.

import { useEffect, useState } from "react"
import {
  WebSocketManager,
  useSocketEvent,
  useSocketSend,
} from "@luciodale/react-socket"

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

const manager = new WebSocketManager<TClientMsg, TServerMsg>({
  url: "wss://your-server.com/ws",
  serialize: (msg) => JSON.stringify(msg),
  deserialize: (raw) => JSON.parse(raw),
})

export function Echo() {
  const [response, setResponse] = useState<string | null>(null)
  const { send } = useSocketSend(manager)

  useSocketEvent(manager, "echo", (msg) => setResponse(msg.text))

  useEffect(() => {
    manager.connect()
    return () => manager.disconnect()
  }, [])

  return (
    <>
      <button onClick={() => send({ type: "echo", text: "hello" })}>
        send
      </button>
      {response && <p>server said: {response}</p>}
    </>
  )
}

Change a field in TClientMsg or TServerMsg and TypeScript lists every call site that needs updating. useSocketEvent narrows the message via Extract<TServerMsg, { type: "echo" }> automatically.

The ten hooks

// React to an incoming message of a given type
useSocketEvent(manager, "notification", (msg) => { /* msg narrowed */ })

// Same as useSocketEvent, but buffers and flushes every flushMs (high-frequency streams)
useSocketEventBatch(manager, "tick", (msgs) => { /* ... */ }, { flushMs: 100 })

// Subscribe to a server-side stream, ref counted, auto cleanup
useSocketSubscription(manager, {
  key: roomId,
  subscribe: { type: "subscribe", channel: roomId },
  unsubscribe: { type: "unsubscribe", channel: roomId },
})

// True while a subscribe is in flight — drives "joining..." UI
const joining = useSocketPendingSubscription(manager, roomId)

// Typed positional send fn
const { send } = useSocketSend(manager)

// Fires on every send(), even offline — drives optimistic UI
useSocketSendIntent(manager, ({ data, ackId }) => { /* ... */ })

// Fires when in-flight messages are dropped on disconnect
useSocketInFlightDrop(manager, (messages) => { /* ... */ })

// Fires after every (re)connect, with the list of restored subscription keys
useSocketReady(manager, (restoredKeys) => { /* ... */ })

// Fires when the last subscriber for a key unmounts.
// 2nd arg is the original subscribe payload (first-payload wins).
useSocketLastUnsubscribe(manager, (key, subscribePayload) => { /* ... */ })

// Observable connection state
const state = useSocketConnectionState(manager)

Autocomplete useSocket in your editor — that is the entire surface.

Acknowledged sends

Tag a message with an ack id, wire the extractor once, the library clears in-flight tracking automatically when the server confirms.

const manager = new WebSocketManager<TClientMsg, TServerMsg>({
  url: "wss://...",
  serialize: JSON.stringify,
  deserialize: (raw) => JSON.parse(raw),

  // library auto-clears the matching in-flight entry when this returns an id
  getAckId: (msg) => (msg.type === "delivered" ? msg.ackId : undefined),
})
const { send } = useSocketSend(manager)

function onSend(text: string) {
  const id = crypto.randomUUID()
  send({ type: "message", id, text }, id) // 2nd arg: ackId
}

Subscriptions

Multiple components with the same key share a single server subscription. The manager dedupes automatically.

function ChatRoom({ roomId }: { roomId: string }) {
  useSocketSubscription(manager, {
    key: roomId,
    subscribe: { type: "subscribe", channel: roomId },
    unsubscribe: { type: "unsubscribe", channel: roomId },
  })

  const joining = useSocketPendingSubscription(manager, roomId)
  return joining ? <span>joining...</span> : <Room id={roomId} />
}

If three components mount ChatRoom with the same roomId, the subscribe message is sent once. When all three unmount, the unsubscribe fires once. Reconnect replays the subscription transparently.

Inspector

A built-in devtools panel for debugging WebSocket traffic. Separate export so it tree-shakes out of production builds.

import { InspectorPanel } from "@luciodale/react-socket/inspector"

function DevTools() {
  return <InspectorPanel manager={manager} />
}

Docs

Full documentation, patterns catalog, configuration reference, and live examples at koolcodez.com/projects/react-socket.

License

MIT