GitHub
Open Github

UseWebSocket

A complete WebSocket hook with auto-reconnect, exponential backoff, message queuing, heartbeat, and full lifecycle callbacks.

Installation

pnpm add @hookraft/usewebsocket

Usage

import { useWebSocket } from "@hookraft/usewebsocket"
const { status, messages, lastMessage, send, disconnect, reconnect } = useWebSocket({
  url: "wss://api.example.com/chat",
  onConnect: () => console.log("Connected!"),
  onDisconnect: () => console.log("Disconnected"),
  onMessage: (data) => console.log("Received:", data),
  onError: (err) => console.error("Error:", err),
  reconnect: true,
  reconnectAttempts: 5,
  heartbeat: { message: "ping", interval: 30000 },
})

The problem it solves

Building a WebSocket connection in React requires handling many things manually — connecting, reconnecting on failure, queuing messages while offline, keeping the connection alive with heartbeats, and cleaning up on unmount. Most developers either skip these features or spend hours getting them right.

// Without useWebSocket — 70+ lines of manual work
const wsRef = useRef(null)
const reconnectTimerRef = useRef(null)
const reconnectAttempts = useRef(0)
const messageQueue = useRef([])

const connect = () => {
  wsRef.current = new WebSocket("wss://api.example.com")

  wsRef.current.onopen = () => {
    // flush queue, reset attempts...
  }
  wsRef.current.onclose = () => {
    // reconnect with backoff...
  }
  wsRef.current.onerror = () => { /* ... */ }
  wsRef.current.onmessage = (e) => { /* parse, store... */ }
}

// You write all of this. Every time.
// With useWebSocket — everything handled
const { status, messages, send } = useWebSocket({
  url: "wss://api.example.com",
  reconnect: true,
  heartbeat: { message: "ping", interval: 30000 },
})

Full Examples

Chat room

A full real-time chat room. Messages arrive instantly, send works even if briefly disconnected (queued and flushed on reconnect).

import { useState } from "react"
import { useWebSocket } from "@hookraft/usewebsocket"

type ChatMessage = {
  id: string
  user: string
  text: string
  timestamp: number
}

function ChatRoom({ roomId }: { roomId: string }) {
  const [input, setInput] = useState("")

  const { status, messages, send, reconnectCount } = useWebSocket<ChatMessage>({
    url: `wss://api.example.com/chat/${roomId}`,
    reconnect: true,
    reconnectAttempts: 10,
    reconnectInterval: 2000,
    heartbeat: {
      message: "ping",
      interval: 30000,
    },
    queueWhileOffline: true,
    onConnect: () => console.log("Joined room"),
    onDisconnect: () => console.log("Left room"),
    onReconnect: (attempt) => console.log(`Reconnecting... attempt ${attempt}`),
    onReconnectFailed: () => console.log("Could not reconnect"),
  })

  const sendMessage = () => {
    if (!input.trim()) return
    send({
      id: crypto.randomUUID(),
      user: "me",
      text: input,
      timestamp: Date.now(),
    })
    setInput("")
  }

  return (
    <div>
      {/* Connection status */}
      <div>
        <span>{status}</span>
        {status === "reconnecting" && (
          <span>Attempt {reconnectCount}</span>
        )}
      </div>

      {/* Messages */}
      <ul>
        {messages.map((msg) => (
          <li key={msg.id}>
            <strong>{msg.user}:</strong> {msg.text}
          </li>
        ))}
      </ul>

      {/* Input */}
      <div>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && sendMessage()}
          placeholder="Type a message..."
          disabled={status === "error"}
        />
        <button onClick={sendMessage} disabled={status === "error"}>
          Send
        </button>
      </div>
    </div>
  )
}

Live dashboard

Real-time data feed — stock prices, sports scores, sensor readings. Reconnects automatically if the connection drops.

import { useWebSocket } from "@hookraft/usewebsocket"

type StockUpdate = {
  symbol: string
  price: number
  change: number
}

function StockTicker() {
  const { lastMessage, status, isConnected } = useWebSocket<StockUpdate>({
    url: "wss://api.example.com/stocks",
    reconnect: true,
    messageLimit: 1, // we only care about the latest price
    onMessage: (data) => console.log(`${data.symbol}: $${data.price}`),
  })

  return (
    <div>
      <span>{isConnected ? "Live" : "Offline"}</span>

      {lastMessage && (
        <div>
          <span>{lastMessage.symbol}</span>
          <span>${lastMessage.price.toFixed(2)}</span>
          <span style={{ color: lastMessage.change >= 0 ? "green" : "red" }}>
            {lastMessage.change >= 0 ? "+" : ""}{lastMessage.change.toFixed(2)}%
          </span>
        </div>
      )}
    </div>
  )
}

Manual connect

Set autoConnect: false to connect only when the user triggers it — useful for opt-in real-time features.

import { useWebSocket } from "@hookraft/usewebsocket"

function LiveNotifications() {
  const { status, messages, connect, disconnect, isConnected } = useWebSocket({
    url: "wss://api.example.com/notifications",
    autoConnect: false, // don't connect on mount
    reconnect: true,
  })

  return (
    <div>
      <button onClick={isConnected ? disconnect : connect}>
        {isConnected ? "Turn off live updates" : "Turn on live updates"}
      </button>

      <p>Status: {status}</p>

      <ul>
        {messages.map((msg, i) => (
          <li key={i}>{String(msg)}</li>
        ))}
      </ul>
    </div>
  )
}

Custom message parser

By default messages are parsed with JSON.parse. Use parseMessage to handle custom formats or filter out certain messages.

import { useWebSocket } from "@hookraft/usewebsocket"

type ServerEvent = {
  type: "message" | "system" | "heartbeat"
  payload: unknown
}

function EventStream() {
  const { messages } = useWebSocket<ServerEvent>({
    url: "wss://api.example.com/events",
    // Custom parser — skip heartbeat messages
    parseMessage: (raw) => {
      try {
        const parsed = JSON.parse(raw) as ServerEvent
        if (parsed.type === "heartbeat") return null // skip
        return parsed
      } catch {
        return null
      }
    },
  })

  return (
    <ul>
      {messages.map((event, i) => (
        <li key={i}>
          [{event.type}] {JSON.stringify(event.payload)}
        </li>
      ))}
    </ul>
  )
}

Send before connected (message queuing)

If queueWhileOffline is true (default), messages sent before the connection opens are queued and automatically flushed when connected.

import { useWebSocket } from "@hookraft/usewebsocket"

function App() {
  const { send, queuedCount, status } = useWebSocket({
    url: "wss://api.example.com",
    queueWhileOffline: true, // default
  })

  return (
    <div>
      <p>Status: {status}</p>
      {queuedCount > 0 && (
        <p>{queuedCount} message(s) queued — will send when connected</p>
      )}

      {/* This works even before the connection opens */}
      <button onClick={() => send({ text: "Hello!" })}>
        Send (queued if offline)
      </button>
    </div>
  )
}

How reconnect works

When the connection drops, useWebSocket waits and tries again using exponential backoff — each retry waits longer than the last, up to a maximum delay.

Attempt 1 → wait 2s  → reconnect
Attempt 2 → wait 4s  → reconnect
Attempt 3 → wait 8s  → reconnect
Attempt 4 → wait 16s → reconnect
Attempt 5 → wait 30s → reconnect (max interval)
             ↓
         onReconnectFailed fires
         status → "error"

You control this with reconnectAttempts, reconnectInterval, and maxReconnectInterval.

How heartbeat works

Some WebSocket servers close idle connections. The heartbeat keeps the connection alive by sending a ping message on a regular interval.

useWebSocket({
  url: "wss://api.example.com",
  heartbeat: {
    message: "ping",    // what to send
    interval: 30000,    // every 30 seconds
  },
})

Responses of "pong" or the same message as the heartbeat are automatically ignored and not added to the message list.

Options

PropTypeDefault
url
string
-
protocols
string | string[]
-
autoConnect
boolean
true
reconnect
boolean
true
reconnectAttempts
number
5
reconnectInterval
number
2000
maxReconnectInterval
number
30000
heartbeat
HeartbeatConfig
-
messageLimit
number
100
queueWhileOffline
boolean
true
onConnect
() => void
-
onDisconnect
(event: CloseEvent) => void
-
onMessage
(data: T, event: MessageEvent) => void
-
onError
(event: Event) => void
-
onReconnect
(attempt: number) => void
-
onReconnectFailed
() => void
-
parseMessage
(raw: string) => T | null
-
serializeMessage
(data: T) => string
-

Returns

PropTypeDefault
status
WebSocketStatus
-
messages
T[]
-
lastMessage
T | null
-
isConnecting
boolean
-
isConnected
boolean
-
isDisconnected
boolean
-
isError
boolean
-
reconnectCount
number
-
queuedCount
number
-
send(data)
(data: T) => void
-
sendRaw(data)
(data: string) => void
-
connect()
() => void
-
disconnect()
() => void
-
reconnect()
() => void
-
clearMessages()
() => void
-
is(status)
(s: WebSocketStatus) => boolean
-