npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-realtime-hooks

v1.0.4

Published

React hooks for WebSocket, EventSource, reconnect, heartbeat, and online status.

Readme

react-realtime-hooks

npm version Quality Gate Demo license TypeScript react

Production-ready React hooks for WebSocket and SSE with auto-reconnect, heartbeat, typed connection state, and browser network awareness.

react-realtime-hooks is for apps that need more than "open a socket and hope for the best". It gives you composable hooks for transport lifecycle, retry strategy, heartbeat, and online status, so your UI can react to realtime state without rebuilding the same connection logic in every screen.

Live demo: https://volkov85.github.io/react-realtime-hooks/

Why This Library

Most realtime helpers stop at transport setup.

Real apps need:

  • explicit connecting / reconnecting / closed / error states
  • reconnect strategy with caps, jitter, and manual control
  • heartbeat and timeout tracking
  • clean SSR behavior
  • browser network awareness
  • typed message parsing and sending

react-realtime-hooks packages those concerns into small hooks that compose cleanly in React.

Killer Features

  • useWebSocket and useEventSource return state you can render, not just transport instances.
  • Built-in reconnect flow with exponential backoff, jitter, attempt limits, and manual restart.
  • Heartbeat support with ack matching, timeout detection, and latency measurement.
  • Discriminated connection snapshots: idle, connecting, open, reconnecting, closing, closed, error.
  • First-class TypeScript support with generic message types and custom parsers/serializers.
  • SSR-safe by default. No browser-only globals are touched during server render.
  • Zero runtime dependencies beyond React.
  • Manual controls stay available when you need them: open(), close(), reconnect(), send().

Raw WebSocket vs This Library

| Concern | Raw WebSocket | react-realtime-hooks | | ----------------- | ---------------------------------- | ------------------------------------------------ | | Connection state | You model it yourself | Built-in status model you can render directly | | Reconnect flow | Manual timers and teardown | useReconnect with backoff, jitter, and limits | | Heartbeat | Custom ping/pong loop | heartbeat support with timeout and latency | | Network awareness | Separate browser event wiring | useOnlineStatus for online/offline state | | SSR safety | Easy to break during render | Browser-only behavior stays out of server render | | UI ergonomics | Event handlers and refs everywhere | Hook result already shaped for product UI |

The point is not to hide WebSocket. The point is to stop rewriting the same lifecycle machinery around it.

Install

npm install react-realtime-hooks

Peer dependency:

  • react@^19.0.0

How It Feels

import { useOnlineStatus, useWebSocket } from "react-realtime-hooks";

type IncomingMessage =
  | { type: "notification"; text: string }
  | { type: "pong" };

type OutgoingMessage = { type: "ack"; id: string } | { type: "ping" };

export function NotificationsPanel() {
  const network = useOnlineStatus();
  const socket = useWebSocket<IncomingMessage, OutgoingMessage>({
    url: "ws://localhost:8080/notifications",
    parseMessage: (event) => JSON.parse(String(event.data)) as IncomingMessage,
    reconnect: {
      initialDelayMs: 1_000,
      maxAttempts: null,
    },
    heartbeat: {
      intervalMs: 10_000,
      timeoutMs: 3_000,
      message: { type: "ping" },
      matchesAck: (message) => message.type === "pong",
    },
  });

  return (
    <section>
      <p>
        Network: {network.isOnline ? "online" : "offline"} | Transport:{" "}
        {socket.status}
      </p>

      {socket.status === "reconnecting" && (
        <p>Retrying in {socket.reconnectState?.nextDelayMs ?? 0}ms</p>
      )}

      {socket.heartbeatState?.hasTimedOut && <p>Heartbeat timed out</p>}

      <button
        disabled={socket.status !== "open"}
        onClick={() => socket.send({ type: "ack", id: "msg-42" })}
      >
        Ack latest
      </button>

      <pre>{JSON.stringify(socket.lastMessage, null, 2)}</pre>
    </section>
  );
}

You are not wiring raw onopen, onclose, and timer cleanup by hand. You render the current transport state and keep moving.

Status-First UX

The transport hooks return a discriminated status model, so UI states stay explicit instead of collapsing into a vague isConnected boolean.

  • idle: auto-connect is off and nothing is opening
  • connecting: first connection attempt is in progress
  • open: transport is live
  • reconnecting: retry flow is active
  • closing: explicit close is in progress
  • closed: transport is stopped and will not continue
  • error: an unrecoverable parse/runtime error occurred

That makes product UI straightforward:

  • show a retry banner on reconnecting
  • disable send buttons unless status === "open"
  • show offline or degraded indicators without guessing
  • surface heartbeat timeout separately from transport close

Architecture Idea

This library is built as layered primitives, not one giant "magic realtime client".

Browser APIs
  WebSocket / EventSource / navigator.onLine

Core hooks
  useReconnect / useHeartbeat / useOnlineStatus

Transport hooks
  useWebSocket / useEventSource

UI
  banners, badges, retry states, feed views, chat inputs

That separation matters:

  • you can use useReconnect and useHeartbeat outside the transport hooks
  • transport hooks stay predictable instead of hiding lifecycle decisions
  • the UI gets a stable state model instead of raw event listeners

Real-World Use Cases

  • Chat and support widgets that need reconnect and delivery-aware UI
  • Notification centers and activity feeds over WebSocket
  • Live dashboards and ops consoles consuming SSE streams
  • Trading, analytics, and monitoring UIs with explicit connection states
  • Device and IoT panels that need heartbeat and timeout visibility
  • Collaborative tools that must reflect degraded or reconnecting transport state

Anti-Features

This package is intentionally not trying to be a full client platform.

  • No bundled transport polyfills
  • No opinionated server protocol
  • No hidden global singleton connection manager
  • No built-in auth refresh flow
  • No state management framework or cache layer
  • No "smart" abstractions that erase transport state details

If you need a predictable hook layer for realtime UI, that is the point. If you need a full messaging platform, this is a lower-level building block.

Why Not Write It Yourself?

Because "just a socket hook" turns into more work than it looks like:

  • reconnect timers need careful cleanup and manual-close semantics
  • heartbeat loops need ack matching, timeout handling, and teardown discipline
  • URL changes and remounts create subtle race conditions
  • SSR breaks if browser globals leak into render
  • a single isOpen flag is not enough for real UI states
  • parse failures and transport errors need consistent state transitions

This library already models those edges in a reusable way.

API At A Glance

| Hook | Use it for | Returns | | ----------------- | ------------------------------------ | ---------------------------------------------------------------------------- | | useWebSocket | Bidirectional realtime channels | status, socket, lastMessage, send(), reconnect(), heartbeatState | | useEventSource | Server-Sent Events streams | status, eventSource, lastMessage, lastEventName, reconnect() | | useReconnect | Reusable retry and backoff logic | schedule(), cancel(), reset(), attempt, status | | useHeartbeat | Liveness checks and timeout tracking | start(), stop(), beat(), notifyAck(), latencyMs | | useOnlineStatus | Browser online/offline state | isOnline, isSupported, transition timestamps |

Transport Examples

useWebSocket

import { useWebSocket } from "react-realtime-hooks";

type IncomingMessage = {
  type: "chat" | "system";
  text: string;
};

type OutgoingMessage = {
  type: "ping" | "chat";
  text?: string;
};

export function ChatSocket() {
  const socket = useWebSocket<IncomingMessage, OutgoingMessage>({
    url: "ws://localhost:8080",
    parseMessage: (event) => JSON.parse(String(event.data)) as IncomingMessage,
    reconnect: {
      initialDelayMs: 1_000,
      maxAttempts: null,
    },
    heartbeat: {
      intervalMs: 10_000,
      timeoutMs: 3_000,
      message: { type: "ping" },
      matchesAck: (message) =>
        message.type === "system" && message.text === "pong",
    },
  });

  return (
    <button onClick={() => socket.send({ type: "chat", text: "Hello" })}>
      Send
    </button>
  );
}

useEventSource

import { useEventSource } from "react-realtime-hooks";

type FeedItem = {
  id: string;
  level: "info" | "warn";
  text: string;
};

export function LiveFeed() {
  const feed = useEventSource<FeedItem>({
    url: "http://localhost:8080/sse",
    events: ["notice"],
    parseMessage: (event) => JSON.parse(event.data) as FeedItem,
    reconnect: {
      initialDelayMs: 1_000,
      maxAttempts: 10,
    },
  });

  return (
    <div>
      {feed.lastEventName}: {feed.lastMessage?.text ?? "Waiting for updates"}
    </div>
  );
}

Core Hook Examples

useReconnect

import { useReconnect } from "react-realtime-hooks";

export function RetryPanel() {
  const reconnect = useReconnect({
    initialDelayMs: 1_000,
    maxAttempts: 5,
    jitterRatio: 0,
  });

  return (
    <button onClick={() => reconnect.schedule("manual")}>Retry now</button>
  );
}

useHeartbeat

import { useHeartbeat } from "react-realtime-hooks";

export function HeartbeatPanel() {
  const heartbeat = useHeartbeat<string, string>({
    intervalMs: 5_000,
    timeoutMs: 2_000,
    startOnMount: true,
    matchesAck: (message) => message === "pong",
  });

  return (
    <div>
      running: {String(heartbeat.isRunning)} | latency:{" "}
      {heartbeat.latencyMs ?? "n/a"}
    </div>
  );
}

useOnlineStatus

import { useOnlineStatus } from "react-realtime-hooks";

export function NetworkIndicator() {
  const network = useOnlineStatus({
    trackTransitions: true,
  });

  return <span>{network.isOnline ? "Online" : "Offline"}</span>;
}

API Reference

Options

| Option | Type | Default | Description | | ------------------ | ------------------------------ | -------------------------- | ---------------------------------- | | url | UrlProvider | Required | String, URL, or lazy URL factory | | protocols | string \| string[] | undefined | WebSocket subprotocols | | connect | boolean | true | Auto-connect on mount | | binaryType | BinaryType | "blob" | Socket binary mode | | parseMessage | (event) => TIncoming | raw event.data | Incoming parser | | serializeMessage | (message) => ... | JSON/string passthrough | Outgoing serializer | | reconnect | false \| UseReconnectOptions | enabled | Reconnect configuration | | heartbeat | false \| UseWebSocketHeartbeatOptions | disabled unless configured | Heartbeat configuration | | shouldReconnect | (event) => boolean | true | Reconnect gate on close | | onOpen | (event, socket) => void | undefined | Open callback | | onMessage | (message, event) => void | undefined | Message callback | | onError | (event) => void | undefined | Called for transport, heartbeat, and parse errors | | onClose | (event) => void | undefined | Close callback |

Result

| Field | Type | Description | | ------------------ | ---------------------------- | -------------------------------------------------------------------------- | | status | connection union | idle, connecting, open, closing, closed, reconnecting, error | | socket | WebSocket \| null | Current transport instance | | lastMessage | TIncoming \| null | Last parsed message | | lastMessageEvent | MessageEvent \| null | Last raw message event | | lastCloseEvent | CloseEvent \| null | Last close event | | lastError | Event \| null | Last error | | bufferedAmount | number | Current socket buffer size | | reconnectState | reconnect snapshot or null | Current reconnect data | | heartbeatState | heartbeat snapshot or null | Current heartbeat data | | open | () => void | Manual connect | | close | (code?, reason?) => void | Manual close | | reconnect | () => void | Manual reconnect | | send | (message) => boolean | Sends an outgoing payload |

When you configure useWebSocket heartbeat, you can also set timeoutAction and errorAction to "none", "close", or "reconnect". The default is "reconnect" when reconnect is enabled and "close" otherwise.

Options

| Option | Type | Default | Description | | ----------------- | ------------------------------------- | ---------------- | ----------------------------------- | | url | UrlProvider | Required | String, URL, or lazy URL factory | | withCredentials | boolean | false | Passes credentials to EventSource | | connect | boolean | true | Auto-connect on mount | | events | readonly string[] | undefined | Named SSE events to subscribe to | | parseMessage | (event) => TMessage | raw event.data | Incoming parser | | reconnect | false \| UseReconnectOptions | enabled | Reconnect configuration | | shouldReconnect | (event) => boolean | true | Reconnect gate on error | | onOpen | (event, source) => void | undefined | Open callback | | onMessage | (message, event) => void | undefined | Default message callback | | onError | (event) => void | undefined | Called for transport and parse errors | | onEvent | (eventName, message, event) => void | undefined | Named event callback |

Result

| Field | Type | Description | | ------------------ | ---------------------------- | -------------------------------------------------------------------------- | | status | connection union | idle, connecting, open, closing, closed, reconnecting, error | | eventSource | EventSource \| null | Current transport instance | | lastEventName | string \| null | Last SSE event name | | lastMessage | TMessage \| null | Last parsed payload | | lastMessageEvent | MessageEvent \| null | Last raw message event | | lastError | Event \| null | Last error | | reconnectState | reconnect snapshot or null | Current reconnect data | | open | () => void | Manual connect | | close | () => void | Manual close | | reconnect | () => void | Manual reconnect |

Options

| Option | Type | Default | Description | | ---------------- | ------------------------ | ----------- | ------------------------------------ | | enabled | boolean | true | Enables scheduling attempts | | initialDelayMs | number | 1000 | Delay for the first attempt | | maxDelayMs | number | 30000 | Delay cap | | backoffFactor | number | 2 | Exponential multiplier | | jitterRatio | number | 0.2 | Randomized variance ratio | | maxAttempts | number \| null | null | Max attempts, null means unlimited | | getDelayMs | ReconnectDelayStrategy | undefined | Custom delay strategy | | resetOnSuccess | boolean | true | Resets attempt count after success | | onSchedule | (attempt) => void | undefined | Called when an attempt is scheduled | | onCancel | () => void | undefined | Called when scheduling is canceled | | onReset | () => void | undefined | Called when state is reset |

Result

| Field | Type | Description | | --------------- | ------------------------------------------------- | ---------------------------------------- | | status | "idle" \| "scheduled" \| "running" \| "stopped" | Current reconnect state | | attempt | number | Current attempt number | | nextDelayMs | number \| null | Delay of the scheduled attempt | | isActive | boolean | true when scheduled or running | | isScheduled | boolean | true when waiting for the next attempt | | schedule | (trigger?) => void | Schedules an attempt | | cancel | () => void | Cancels the current schedule | | reset | () => void | Resets attempts and status | | markConnected | () => void | Marks the transport as restored |

Options

| Option | Type | Default | Description | | -------------- | --------------------------------------------------- | ----------- | ------------------------------------------- | | enabled | boolean | true | Enables the heartbeat loop | | intervalMs | number | Required | Beat interval | | timeoutMs | number | undefined | Timeout before hasTimedOut becomes true | | message | TOutgoing \| (() => TOutgoing) | undefined | Optional heartbeat payload | | beat | () => void \| boolean \| Promise<void \| boolean> | undefined | Custom beat side effect | | matchesAck | (message) => boolean | undefined | Ack matcher | | startOnMount | boolean | true | Starts immediately | | onBeat | () => void | undefined | Called on every beat | | onTimeout | () => void | undefined | Called on timeout | | onError | (error) => void | undefined | Called when beat() throws or rejects |

Result

| Field | Type | Description | | ------------- | ---------------------- | --------------------------------- | | isRunning | boolean | Whether the loop is active | | hasTimedOut | boolean | Whether the latest beat timed out | | lastBeatAt | number \| null | Last beat timestamp | | lastAckAt | number \| null | Last ack timestamp | | latencyMs | number \| null | Ack latency | | start | () => void | Starts the loop | | stop | () => void | Stops the loop | | beat | () => void | Triggers a manual beat | | notifyAck | (message) => boolean | Applies an incoming ack message |

Options

| Option | Type | Default | Description | | ------------------ | --------- | ------- | ------------------------------------------------------- | | initialOnline | boolean | true | Fallback value when navigator.onLine is unavailable | | trackTransitions | boolean | true | Tracks lastChangedAt, wentOnlineAt, wentOfflineAt |

Result

| Field | Type | Description | | --------------- | ---------------- | ---------------------------------------- | | isOnline | boolean | Current browser online state | | isSupported | boolean | Whether navigator.onLine is available | | lastChangedAt | number \| null | Timestamp of the last transition | | wentOnlineAt | number \| null | Timestamp of the last online transition | | wentOfflineAt | number \| null | Timestamp of the last offline transition |

Limitations And Edge Cases

  • useEventSource is receive-only by design. SSE is not a bidirectional transport.
  • useWebSocket heartbeat support is client-side. You still define your own server ping/pong protocol.
  • If parseMessage throws, the hook calls onError, closes the current transport, moves into error, stores lastError, and stops auto-reconnect until manual open() or reconnect().
  • Stopping heartbeat clears timeout state and the previous beat/ack timestamps so a new session starts with fresh metrics.
  • connect: false keeps the hook in idle until open() is called.
  • Manual close() is sticky. The hook stays closed until open() or reconnect() is called.
  • No transport polyfills are bundled. Provide your own runtime support where needed.
  • Browser-native transport constraints still apply: auth, proxy, CORS, and network policy are outside the hook's control.

Testing And Quality

The package includes behavior tests for:

  • connect / disconnect / reconnect
  • exponential backoff
  • timer and listener cleanup
  • heartbeat start / stop / timeout
  • browser offline / online transitions
  • invalid payload and parse errors
  • manual reconnect and manual close

WebSocket and EventSource are tested through mocked browser APIs.

Demo

  • Live demo: https://volkov85.github.io/react-realtime-hooks/
  • Repository: https://github.com/volkov85/react-realtime-hooks

Run the local playground:

npm run demo

Contributing

Development and release workflow live in CONTRIBUTING.md.

License

MIT