Skip to content
rhttp.io

Realtime & Socket.io

rhttp.io ships a Socket.io realtime client with rooms, offline queuing, event validation, CSRF protection, and a full suite of React hooks — all behind the rhttp.io/socket.io.client entry point.

Quick start

import {
  createRealtimeClient,
} from "rhttp.io/socket.io.client"; 
 
const rt = createRealtimeClient({
  socketUrl: "https://api.example.com", 
  transports: ["websocket"],
  auth: {
    getToken: async () => localStorage.getItem("jwt"),
  },
  csrf: {
    enabled: true,
    fetchEndpoint: "/api/csrf",
  },
  reconnection: true,
  hooks: {
    onConnect: () => console.log("Connected"),
    onDisconnect: (reason) => console.warn("Disconnected:", reason),
  },
});
 
// Connect and start receiving events.
await rt.connect(); 
 
rt.on("message", (data) => {
  console.log("New message:", data);
});
 
// Send events.
rt.emit("chat:message", { text: "Hello!" });
const reply = await rt.emitWithAck("chat:message", { text: "Hello!" }, 5_000); 
 
// Rooms.
await rt.joinRoom("general");
rt.getRooms(); // ["general"]
await rt.leaveRoom("general");

createRealtimeClient(config)

Creates a new RealtimeClient instance. The only required field is socketUrl.

import { createRealtimeClient } from "rhttp.io/socket.io.client";
import type { RealtimeClientConfig } from "rhttp.io/socket.io.client";
 
const rt = createRealtimeClient({
  socketUrl: string,        // required — Socket.io server URL
 
  withCredentials?: boolean,          // default: true
  extraHeaders?: Record<string, string>,
  transports?: string[],               // default: ["websocket", "polling"]
 
  // Authentication
  auth?: {
    token?: string,                    // static token
    scheme?: string,                   // e.g. "Bearer"
    getToken?: () => Promise<string | null>,
    authFactory?: () => Promise<Record<string, any>>,
  },
 
  // CSRF protection
  csrf?: {
    enabled: boolean,
    fetchEndpoint: string,
    headerName?: string,               // default: "X-CSRF-Token"
    cookieName?: string,               // default: "csrf-token"
    fetchOptions?: RequestInit,
  },
 
  // Room management
  rooms?: {
    autoRejoin?: boolean,              // rejoin rooms on reconnect
    autoJoin?: string[],               // rooms to join on connect
  },
 
  // Offline queue
  offlineQueue?: {
    enabled: boolean,
    maxSize?: number,                  // default: 100
    storageKey?: string,               // default: "rhttp.io/realtime:offline-queue"
  },
 
  // Reconnection
  reconnection?: boolean,             // default: true
  reconnectionDelay?: number,          // default: 1000 ms
  reconnectionDelayMax?: number,       // default: 5000 ms
  reconnectionAttempts?: number,        // default: Infinity
 
  // Advanced Socket.io options
  socketOptions?: Record<string, any>,
 
  // Event validation & transformation
  eventValidator?: (event: string, data: any, direction: "emit" | "receive") => boolean | Promise<boolean>,
  eventTransformer?: (event: string, data: any, direction: "emit" | "receive") => any | Promise<any>,
 
  // Lifecycle hooks
  hooks?: {
    onConnect?: () => void | Promise<void>,
    onDisconnect?: (reason: string) => void | Promise<void>,
    onError?: (error: any) => void | Promise<void>,
  },
 
  logger?: boolean | CustomLogger,
});

Connection lifecycle

const rt = createRealtimeClient({ socketUrl: "https://api.example.com" });
 
// Connect (idempotent — no-op if already connected).
await rt.connect(); 
// The connection attempt times out after 10 seconds, throwing ConnectionError.
 
// Check state.
rt.isConnected;     // boolean
rt.isConnecting;    // boolean
rt.isReconnecting;   // boolean
 
// Subscribe to state changes.
const unsubscribe = rt.onStateChange((state) => {
  console.log(state); // { connected, connecting, reconnecting }
}); 
unsubscribe(); // stop listening
 
// Disconnect (keeps instance reusable — can call connect() again).
rt.disconnect(); 
 
// Destroy (disconnects + clears rooms + state listeners + offline queue).
rt.destroy(); 

Event emission

emit(event, data?, callback?)

Fire-and-forget. If the client is disconnected and the offline queue is enabled, the event is queued and flushed on reconnect.

rt.emit("chat:message", { text: "Hello" });
 
// With acknowledgment callback.
rt.emit("chat:message", { text: "Hello" }, (response) => {
  console.log("Server acknowledged:", response);
}); 

emitWithAck(event, data?, timeout?)

Returns a promise. Rejects with TimeoutError if no ack arrives within the timeout (default: 5000 ms). Rejects with ConnectionError if not connected.

const reply = await rt.emitWithAck("chat:message", { text: "Hello" }, 5_000); 

Event validation & transformation

Both emit and event listeners run through the validation/transform pipeline:

const rt = createRealtimeClient({
  socketUrl: "https://api.example.com",
  eventValidator: (event, data, direction) => {
    if (direction === "emit" && event === "chat:message") {
      return typeof data?.text === "string"; 
    }
    return true;
  },
  eventTransformer: (event, data, direction) => {
    if (direction === "receive" && event === "chat:message") {
      return { ...data, timestamp: Date.now() }; 
    }
    return data;
  },
});

Events that fail validation are silently dropped (not emitted / not forwarded to listeners).

Rooms

// Join a room. If disconnected, the join is queued.
await rt.joinRoom("general"); 
 
// Leave a room (best-effort — resolves even if disconnected).
await rt.leaveRoom("general"); 
 
// Check room membership.
rt.getRooms();       // string[]
rt.isInRoom("general"); // boolean

Room management communicates via internal Socket.io events (join:room / leave:room). If the server returns an error, joinRoom rejects with RoomError.

Auto-rejoin

Enable rooms.autoRejoin: true to automatically rejoin all previously joined rooms when the socket reconnects:

const rt = createRealtimeClient({
  socketUrl: "https://api.example.com",
  rooms: { autoRejoin: true, autoJoin: ["notifications"] }, 
});

Offline queue

When enabled, events emitted while disconnected are persisted to localStorage and replayed on reconnect.

const rt = createRealtimeClient({
  socketUrl: "https://api.example.com",
  offlineQueue: { enabled: true, maxSize: 200 }, 
});
 
// Emit while disconnected — queued automatically.
rt.emit("chat:message", { text: "Stored for later" });
 
// Reconnect — queue is flushed.
await rt.connect(); // auto-flushes queued messages
 
// Manual queue management.
rt.getQueueLength();  // number
rt.clearQueue();     // discard all
await rt.flushQueue(); // replay immediately

CSRF for realtime

The realtime CSRF handler works like the HTTP client's CSRF protection — it fetches a token from the configured endpoint and injects it as an extra header when connecting.

const rt = createRealtimeClient({
  socketUrl: "https://api.example.com",
  csrf: {
    enabled: true,
    fetchEndpoint: "/api/csrf",
    headerName: "X-CSRF-Token",
    cookieName: "csrf-token",
  }, 
});

The CSRF token is prefetched during connect() and included in the Socket.io handshake headers.

React integration

The realtime module ships a <RealtimeProvider> and a suite of hooks for React 18+ (SSR-safe via useSyncExternalStore).

Provider

import { createRealtimeClient, RealtimeProvider } from "rhttp.io/socket.io.client";
 
const rt = createRealtimeClient({
  socketUrl: "https://api.example.com",
  auth: { getToken: () => localStorage.getItem("jwt") },
});
 
function App() {
  return (
    <RealtimeProvider client={rt} autoConnect> 
      <ChatRoom />
    </RealtimeProvider>
  );
}

RealtimeProvider props:

PropTypeDefaultDescription
clientRealtimeClientInstanceRequired
autoConnectbooleantrueConnect on mount
prefetchCsrfbooleantruePrefetch CSRF token on mount
onConnected() => voidCallback on connection
onDisconnected(reason: string) => voidCallback on disconnection
onError(error: Error) => voidCallback on error

Hooks

import {
  useConnectionState,
  useSocketEvent,
  useSocketEmit,
  useSocketEmitWithAck,
  useRoomEvent,
  useRoomManagement,
  useOfflineQueue,
  useConnectionMetrics,
  useSocketConnection,
} from "rhttp.io/socket.io.client"; 

useConnectionState()

Returns { connected: boolean; connecting: boolean; reconnecting: boolean }. SSR-safe.

function StatusIndicator() {
  const state = useConnectionState();
  return state.connected ? <span>🟢 Online</span> : <span>🔴 Offline</span>;
}

useSocketEvent(event, handler)

Subscribes to a Socket.io event, auto-cleans up on unmount.

function MessageList() {
  const [messages, setMessages] = useState<string[]>([]);
 
  useSocketEvent("chat:message", (data) => { 
    setMessages((prev) => [...prev, data.text]);
  });
 
  return <ul>{messages.map((m, i) => <li key={i}>{m}</li>)}</ul>;
}

useSocketEmit() / useSocketEmitWithAck()

function ChatInput() {
  const send = useSocketEmit(); 
 
  const handleSubmit = (text: string) => {
    send("chat:message", { text });
  };
 
  return <input onSubmit={(e) => handleSubmit(e.target.value)} />;
}
 
// With acknowledgment:
function PingButton() {
  const sendWithAck = useSocketEmitWithAck(); 
 
  const handlePing = async () => {
    const reply = await sendWithAck("ping", null, 3_000);
    console.log(reply);
  };
 
  return <button onClick={handlePing}>Ping</button>;
}

useRoomEvent(room, event, handler)

Joins a room on mount, listens for events, leaves on unmount.

function RoomChat({ room }: { room: string }) {
  const [messages, setMessages] = useState<string[]>([]);
 
  useRoomEvent(room, "chat:message", (data) => { 
    setMessages((prev) => [...prev, data.text]);
  });
 
  return <ul>{messages.map((m, i) => <li key={i}>{m}</li>)}</ul>;
}

useRoomManagement()

function RoomControls() {
  const { joinRoom, leaveRoom, getRooms, isInRoom } = useRoomManagement(); 
 
  return (
    <div>
      <button onClick={() => joinRoom("general")}>Join General</button>
      <button onClick={() => leaveRoom("general")}>Leave General</button>
      <p>Rooms: {getRooms().join(", ")}</p>
    </div>
  );
}

useOfflineQueue() / useConnectionMetrics()

function QueueStatus() {
  const { getQueueLength, clearQueue, flushQueue } = useOfflineQueue(); 
  return <span>Queued: {getQueueLength()}</span>;
}
 
function Metrics() {
  const metrics = useConnectionMetrics(); 
  return <pre>{JSON.stringify(metrics, null, 2)}</pre>;
  // { totalEventsEmitted, totalEventsReceived, reconnectAttempts, queuedMessages, uptime, … }
}

useSocketConnection()

Returns the full connection API: { connected, connecting, reconnecting, connect, disconnect }.

Singleton helpers

For apps that use a single global Socket.io connection, use the singleton helpers:

import {
  initializeSocketClient,
  getSocketClient,
  socket,
} from "rhttp.io/socket.io.client"; 
 
// Initialize once (in your app entry point).
initializeSocketClient({
  socketUrl: "https://api.example.com",
  auth: { getToken: () => localStorage.getItem("jwt") },
});
 
// Access anywhere.
const client = getSocketClient();
await socket.connect();
socket.emit("event", data);
socket.disconnect();
socket.destroy();

SSR safety

The realtime client is SSR-safe: if typeof window === "undefined", the constructor and connect() return early without creating a socket. All hooks that use useSyncExternalStore provide a server snapshot, so they won't hydrate-mismatch.