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"); // booleanRoom 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 immediatelyCSRF 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:
| Prop | Type | Default | Description |
|---|---|---|---|
client | RealtimeClientInstance | — | Required |
autoConnect | boolean | true | Connect on mount |
prefetchCsrf | boolean | true | Prefetch CSRF token on mount |
onConnected | () => void | — | Callback on connection |
onDisconnected | (reason: string) => void | — | Callback on disconnection |
onError | (error: Error) => void | — | Callback 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.
Related
- Realtime API Reference — all types and interfaces.
- React & TanStack Query — HTTP queries in React.
- CSRF Protection — HTTP-level CSRF (separate from realtime CSRF).