
Web Realtime Socket Io
- 43 installs
- 19 repo stars
- Updated July 19, 2026
- agents-inc/skills
web-realtime-socket-io is a Claude Code skill that provides Socket.IO v4.x client patterns for real-time communication, covering rooms, namespaces, acknowledgments, auth, and reconnection.
About
This skill teaches an agent Socket.IO v4.x real-time communication patterns. It covers typed ServerToClientEvents and ClientToServerEvents interfaces, the connection lifecycle and reconnection, authentication via the auth option rather than query strings, rooms and namespaces for logical grouping, acknowledgments, connection state recovery, and React integration hooks. It stresses that Socket.IO is not a plain WebSocket implementation. A developer uses it when building real-time features like chat or multiplayer that need rooms, namespaces, or transport fallback.
- Socket.IO v4.x client patterns and connection lifecycle
- Rooms, namespaces, acknowledgments, and reconnection
- Typed event interfaces and auth-token handling
Web Realtime Socket Io by the numbers
- 43 all-time installs (skills.sh)
- Ranked #1,350 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
web-realtime-socket-io capabilities & compatibility
- Capabilities
- socket connection · rooms namespaces · acknowledgments · reconnection · realtime auth
- Use cases
- frontend · api development
What web-realtime-socket-io says it does
Socket.IO is NOT a WebSocket implementation - it adds a protocol layer with additional features.
You MUST use the `auth` option for authentication tokens - NEVER pass tokens in query strings
npx skills add https://github.com/agents-inc/skills --skill web-realtime-socket-ioAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 43 |
|---|---|
| repo stars | ★ 19 |
| Last updated | July 19, 2026 |
| Repository | agents-inc/skills ↗ |
What it does
Build Socket.IO v4.x real-time features with typed events, rooms, namespaces, acknowledgments, auth, and reconnection.
Who is it for?
Building real-time features requiring rooms or namespaces, such as chat and multiplayer, with automatic reconnection.
Skip if: Simple WebSocket needs without rooms or namespaces, connecting to non-Socket.IO servers, or when minimal bundle size is critical.
When should I use this skill?
Building Socket.IO client connections, rooms, namespaces, acknowledgments, or authenticated real-time features.
By the numbers
- Socket.IO v4.x, connection state recovery in v4.6.0+
- Socket.IO adds ~14.5KB gzipped overhead
Files
Socket.IO Real-Time Communication Patterns
Quick Guide: Use Socket.IO for real-time bidirectional communication when you need rooms, namespaces, automatic reconnection, acknowledgments, or transport fallback. Socket.IO is NOT a WebSocket implementation - it adds a protocol layer with additional features. Always define typed event interfaces, use the auth option for tokens (never query strings), and clean up listeners on unmount.---
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)(You MUST define typed interfaces for ALL Socket.IO events - ServerToClientEvents and ClientToServerEvents)
(You MUST use the `auth` option for authentication tokens - NEVER pass tokens in query strings)
(You MUST clean up event listeners on component unmount using socket.off())
(You MUST handle connection errors and implement proper reconnection state management)
(You MUST use named constants for all timeout values, retry limits, and intervals)
</critical_requirements>
---
Auto-detection: Socket.IO, socket.io-client, io(), useSocket, socket.emit, socket.on, rooms, namespaces, acknowledgments, real-time
When to use:
- Building real-time features requiring rooms or namespaces (chat, multiplayer)
- Need automatic reconnection with connection state recovery
- Need acknowledgments/callbacks for message delivery confirmation
- Building applications that must work in restrictive network environments (fallback transports)
- Need server-side broadcasting patterns (emit to room, namespace, all clients)
Key patterns covered:
- TypeScript event interfaces (ServerToClientEvents, ClientToServerEvents)
- Client connection configuration and lifecycle
- Authentication via auth option and middleware
- Rooms and namespaces for logical grouping
- Acknowledgments and callbacks
- Connection state recovery (v4.6.0+)
- React integration hooks
When NOT to use:
- Simple WebSocket needs without rooms/namespaces (use native WebSocket)
- Need to connect to non-Socket.IO WebSocket servers (incompatible protocols)
- Minimal bundle size is critical (Socket.IO adds ~14.5KB gzipped overhead)
Detailed Resources:
- examples/core.md - Socket factory, React hooks, event listeners, message queue, typing indicators, volatile events, namespace multiplexing
- examples/authentication.md - Token auth, cookie auth, token refresh, namespace auth, auth state machine
- examples/rooms.md - Room manager, room hooks, multi-room chat, namespace sockets, conditional namespace access
- reference.md - Decision frameworks, client options reference, checklists
---
<philosophy>
Philosophy
Socket.IO provides a layer on top of WebSocket with additional features: automatic reconnection, room-based broadcasting, acknowledgments, and transport fallback. It is NOT a WebSocket implementation - a plain WebSocket client cannot connect to a Socket.IO server and vice versa.
Key Architectural Concepts:
1. Transport Abstraction: Socket.IO uses WebSocket when available but falls back to HTTP long-polling for restrictive networks. Default order: polling first, then upgrade to WebSocket.
2. Rooms: Server-side grouping mechanism for targeted broadcasting. Clients don't know about rooms - they're purely a server concept for organizing sockets.
3. Namespaces: Separate communication channels on the same connection. Used to separate concerns (e.g., /chat, /admin, /notifications). Each can have its own middleware.
4. Connection State Recovery (v4.6.0+): Missed events can be automatically delivered after brief disconnections, reducing manual state sync. Server-configurable with 2-minute default window.
Connection Lifecycle:
CONNECTING -> CONNECTED <-> (events) -> DISCONNECTING -> DISCONNECTED
| |
(error) <- reconnect <- (disconnect)Socket.IO vs Native WebSocket:
| Feature | Socket.IO | Native WebSocket |
|---|---|---|
| Transport fallback | Automatic | Manual |
| Reconnection | Built-in | Manual |
| Rooms | Built-in | Manual (server-side) |
| Namespaces | Built-in | Not available |
| Acknowledgments | Built-in | Manual |
| Protocol | Custom (incompatible) | Standard WebSocket |
| Bundle size | ~14.5KB gzipped | Native (0KB) |
</philosophy>
---
<patterns>
Core Patterns
Pattern 1: TypeScript Event Interfaces
Define separate interfaces for each communication direction. Socket.IO v4 enforces these at compile time.
interface ServerToClientEvents {
"message:received": (message: ChatMessage) => void;
"user:joined": (user: User) => void;
error: (error: SocketError) => void;
}
interface ClientToServerEvents {
"message:send": (
content: string,
callback: (res: MessageResponse) => void,
) => void;
"room:join": (roomId: string, callback: (result: JoinResult) => void) => void;
}
type TypedSocket = Socket<ServerToClientEvents, ClientToServerEvents>;Why this matters: Without typed events, typos in event names fail silently at runtime. Typed interfaces catch "mesage" vs "message" at compile time.
See examples/core.md Example 1 for complete type definitions.
---
Pattern 2: Client Configuration
Token goes in auth object (never query string). Use named constants for all timing values. The timeout option controls the connection timeout (default 20000ms). For acknowledgment timeouts, use ackTimeout (v4.6.0+) or socket.timeout(ms).emitWithAck().
const RECONNECTION_DELAY_MS = 1000;
const MAX_RECONNECTION_ATTEMPTS = 10;
const CONNECTION_TIMEOUT_MS = 20000;
const socket: TypedSocket = io(url, {
auth: { token }, // NOT in query string
reconnectionAttempts: MAX_RECONNECTION_ATTEMPTS,
reconnectionDelay: RECONNECTION_DELAY_MS,
timeout: CONNECTION_TIMEOUT_MS, // Connection timeout
transports: ["websocket", "polling"],
});Key distinction: timeout = connection timeout. ackTimeout = per-emit acknowledgment timeout (requires retries option, v4.6.0+).
See examples/core.md Example 1 for full factory implementation.
---
Pattern 3: Connection Lifecycle
Socket-level events (connect, disconnect, connect_error) track the socket. Manager-level events (reconnect_attempt, reconnect, reconnect_failed) track the underlying connection. Always listen to both.
socket.on("connect", () => {
/* connected */
});
socket.on("disconnect", (reason) => {
// socket.active === true means it will reconnect
});
socket.on("connect_error", (error) => {
/* handle */
});
// Manager-level: socket.io is the Manager instance
socket.io.on("reconnect_attempt", (attempt) => {
/* show UI */
});
socket.io.on("reconnect_failed", () => {
/* all attempts exhausted */
});Critical: Check socket.recovered (v4.6.0+) after connect to determine if missed events were automatically delivered or if you need a full state refresh.
See examples/core.md Examples 2-3 for React hooks.
---
Pattern 4: Acknowledgments
Two approaches: automatic retries (v4.6.0+) or manual emitWithAck. Both confirm message delivery.
// Automatic retries (v4.6.0+)
const socket = io(url, { ackTimeout: 5000, retries: 3 });
socket.emit("message:send", content, (response) => {
/* confirmed */
});
// Manual with emitWithAck
const response = await socket
.timeout(5000)
.emitWithAck("message:send", content);Gotcha: When using automatic retries, server handlers must be idempotent since the same packet may arrive multiple times.
See examples/core.md Example 4 for the emit hook pattern.
---
Pattern 5: Auth Token Handling
The auth option can be an object (evaluated once) or a function (called on every connection/reconnection). Use the function form to ensure fresh tokens on reconnect.
// Static (stale on reconnect)
const socket = io(url, { auth: { token } });
// Dynamic (fresh on every connection attempt)
const socket = io(url, {
auth: (cb) => {
cb({ token: getToken() });
},
});
// Or update before reconnection
socket.io.on("reconnect_attempt", () => {
socket.auth = { token: getToken() };
});See examples/authentication.md for full auth patterns, token refresh, and auth state machine.
---
Pattern 6: Rooms and Namespaces
Rooms are server-side only - clients request to join, server decides. Namespaces are protocol-level - clients connect explicitly. Multiple namespace sockets share one underlying connection via the Manager.
// Namespaces: use Manager for connection sharing
const manager = new Manager(url, { autoConnect: false });
const chatSocket = manager.socket("/chat");
const adminSocket = manager.socket("/admin", {
auth: { token: adminToken }, // Per-namespace auth
});
manager.connect();See examples/rooms.md for room manager, room hooks, and namespace patterns.
---
Pattern 7: Listener Cleanup
Every socket.on() must have a corresponding socket.off(). In React, return cleanup from useEffect. Pass the exact same function reference to off().
useEffect(() => {
const handler = (msg: Message) => setMessages((prev) => [...prev, msg]);
socket.on("message", handler);
return () => {
socket.off("message", handler);
}; // Same reference
}, [socket]);Why this matters: Without cleanup, handlers accumulate on re-renders causing memory leaks and duplicate processing.
</patterns>
---
<red_flags>
RED FLAGS
- Token in query string - Visible in server logs, browser history, proxy logs. Always use
authoption. - No event type definitions - Typos in event names fail silently. Define
ServerToClientEvents/ClientToServerEvents. - Missing socket.off() cleanup - Memory leaks and duplicate handlers accumulate.
- No connection error handling - Users see blank screens with no feedback on failures.
- Using socket.id as user identifier - Changes on every reconnection. Use server-provided user ID.
- Sending without connected check -
socket.emit()on a disconnected socket fails silently. Checksocket.connectedor queue messages. - Confusing `timeout` with `ackTimeout` -
timeoutis connection timeout (default 20000ms).ackTimeoutis acknowledgment timeout (v4.6.0+, requiresretries). - Static auth with long sessions - Token expires, reconnection fails. Use
authas a function or update onreconnect_attempt.
Gotchas:
- Socket.IO protocol is incompatible with plain WebSocket - they cannot interoperate
- Default transport order is polling-first, then upgrade to WebSocket (not WebSocket-first)
socket.recoveredonly works when server has connection state recovery enabled (v4.6.0+)- Namespaces share one WebSocket connection - a transport failure affects all namespaces
- Rooms are purely server-side - the client never knows which rooms it belongs to
volatile.emit()may silently drop messages - only use for expendable data (cursor positions)
</red_flags>
---
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST define typed interfaces for ALL Socket.IO events - ServerToClientEvents and ClientToServerEvents)
(You MUST use the `auth` option for authentication tokens - NEVER pass tokens in query strings)
(You MUST clean up event listeners on component unmount using socket.off())
(You MUST handle connection errors and implement proper reconnection state management)
(You MUST use named constants for all timeout values, retry limits, and intervals)
Failure to follow these rules will result in security vulnerabilities, memory leaks, and type-unsafe code.
</critical_reminders>
Socket.IO Authentication Examples
Patterns for authentication, authorization, token handling, and reconnection with credential refresh in Socket.IO v4.x.
---
Authentication Approaches
| Approach | Security | Use Case |
|---|---|---|
auth option | High | Modern approach - token in handshake |
| First message | Medium | Legacy - token sent after connection |
| Query string | Low (avoid) | Tokens visible in logs |
| Cookies | High | Session-based authentication |
---
Example 1: Token-Based Authentication
The recommended approach using the auth option.
Constants
const AUTH_ERROR_CODES = {
INVALID_TOKEN: "INVALID_TOKEN",
TOKEN_EXPIRED: "TOKEN_EXPIRED",
UNAUTHORIZED: "UNAUTHORIZED",
} as const;
const TOKEN_REFRESH_BUFFER_MS = 60000; // Refresh 1 minute before expiryTypes
// types/auth-events.ts
interface AuthServerEvents {
"auth:success": (data: { userId: string; sessionId: string }) => void;
"auth:error": (error: AuthError) => void;
"auth:refresh_required": () => void;
}
interface AuthClientEvents {
"auth:refresh": (
token: string,
callback: (result: RefreshResult) => void,
) => void;
}
interface AuthError {
code: string;
message: string;
}
interface RefreshResult {
success: boolean;
error?: string;
}
interface JWTPayload {
sub: string;
exp: number;
iat: number;
}
export type {
AuthServerEvents,
AuthClientEvents,
AuthError,
RefreshResult,
JWTPayload,
};Implementation
// lib/socket-auth.ts
import { io, Socket } from "socket.io-client";
import type {
AuthServerEvents,
AuthClientEvents,
AuthError,
JWTPayload,
} from "../types/auth-events";
const SOCKET_URL = process.env.SOCKET_URL ?? "http://localhost:3001";
const RECONNECTION_ATTEMPTS = 5;
const RECONNECTION_DELAY_MS = 1000;
type AuthSocket = Socket<AuthServerEvents, AuthClientEvents>;
interface AuthConfig {
getToken: () => string | null;
refreshToken: () => Promise<string | null>;
onAuthError: (error: AuthError) => void;
onConnect: (data: { userId: string; sessionId: string }) => void;
}
export function createAuthenticatedSocket(config: AuthConfig): AuthSocket {
const { getToken, refreshToken, onAuthError, onConnect } = config;
const token = getToken();
if (!token) {
throw new Error("No authentication token available");
}
const socket: AuthSocket = io(SOCKET_URL, {
auth: { token },
autoConnect: false,
reconnection: true,
reconnectionAttempts: RECONNECTION_ATTEMPTS,
reconnectionDelay: RECONNECTION_DELAY_MS,
transports: ["websocket", "polling"],
});
// Handle successful authentication
socket.on("auth:success", onConnect);
// Handle authentication errors
socket.on("auth:error", (error) => {
onAuthError(error);
socket.disconnect();
});
// Handle token refresh request from server
socket.on("auth:refresh_required", async () => {
const newToken = await refreshToken();
if (newToken) {
// Update auth and emit refresh
socket.auth = { token: newToken };
socket.emit("auth:refresh", newToken, (result) => {
if (!result.success) {
onAuthError({ code: "REFRESH_FAILED", message: result.error ?? "" });
socket.disconnect();
}
});
} else {
onAuthError({
code: "REFRESH_FAILED",
message: "Could not refresh token",
});
socket.disconnect();
}
});
// Handle reconnection - update token before reconnecting
socket.io.on("reconnect_attempt", async () => {
const currentToken = getToken();
if (currentToken) {
socket.auth = { token: currentToken };
}
});
return socket;
}
// Parse JWT to check expiration (without verification)
export function parseJWT(token: string): JWTPayload | null {
try {
const base64Payload = token.split(".")[1];
const payload = JSON.parse(atob(base64Payload));
return payload as JWTPayload;
} catch {
return null;
}
}
// Check if token will expire soon
export function isTokenExpiringSoon(
token: string,
bufferMs: number = TOKEN_REFRESH_BUFFER_MS,
): boolean {
const payload = parseJWT(token);
if (!payload) return true;
const expiresAt = payload.exp * 1000; // Convert to milliseconds
const now = Date.now();
return now + bufferMs >= expiresAt;
}---
Example 2: Auth Hook for React
React hook that manages authentication lifecycle.
// hooks/use-socket-auth.ts
import { useEffect, useState, useCallback, useRef } from "react";
import type { Socket } from "socket.io-client";
import {
createAuthenticatedSocket,
isTokenExpiringSoon,
} from "../lib/socket-auth";
import type { AuthError } from "../types/auth-events";
const TOKEN_CHECK_INTERVAL_MS = 30000; // Check every 30 seconds
interface UseSocketAuthOptions {
getToken: () => string | null;
refreshToken: () => Promise<string | null>;
onAuthError?: (error: AuthError) => void;
}
interface UseSocketAuthResult {
socket: Socket | null;
isAuthenticated: boolean;
isConnecting: boolean;
error: AuthError | null;
connect: () => void;
disconnect: () => void;
}
export function useSocketAuth(
options: UseSocketAuthOptions,
): UseSocketAuthResult {
const { getToken, refreshToken, onAuthError } = options;
const [socket, setSocket] = useState<Socket | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState<AuthError | null>(null);
const tokenCheckInterval = useRef<ReturnType<typeof setInterval> | null>(
null,
);
// Proactive token refresh
useEffect(() => {
if (!isAuthenticated) return;
tokenCheckInterval.current = setInterval(async () => {
const token = getToken();
if (token && isTokenExpiringSoon(token)) {
const newToken = await refreshToken();
if (newToken && socket) {
socket.auth = { token: newToken };
}
}
}, TOKEN_CHECK_INTERVAL_MS);
return () => {
if (tokenCheckInterval.current) {
clearInterval(tokenCheckInterval.current);
}
};
}, [isAuthenticated, socket, getToken, refreshToken]);
const connect = useCallback(() => {
const token = getToken();
if (!token) {
setError({ code: "NO_TOKEN", message: "No authentication token" });
return;
}
setIsConnecting(true);
setError(null);
try {
const newSocket = createAuthenticatedSocket({
getToken,
refreshToken,
onAuthError: (err) => {
setError(err);
setIsAuthenticated(false);
setIsConnecting(false);
onAuthError?.(err);
},
onConnect: () => {
setIsAuthenticated(true);
setIsConnecting(false);
},
});
// Handle connection events
newSocket.on("connect", () => {
setIsConnecting(false);
});
newSocket.on("disconnect", () => {
setIsAuthenticated(false);
});
newSocket.on("connect_error", () => {
setIsConnecting(false);
});
setSocket(newSocket);
newSocket.connect();
} catch (err) {
setError({
code: "CONNECT_ERROR",
message: err instanceof Error ? err.message : "Connection failed",
});
setIsConnecting(false);
}
}, [getToken, refreshToken, onAuthError]);
const disconnect = useCallback(() => {
socket?.disconnect();
setSocket(null);
setIsAuthenticated(false);
setError(null);
}, [socket]);
// Cleanup on unmount
useEffect(() => {
return () => {
socket?.disconnect();
if (tokenCheckInterval.current) {
clearInterval(tokenCheckInterval.current);
}
};
}, [socket]);
return {
socket,
isAuthenticated,
isConnecting,
error,
connect,
disconnect,
};
}---
Example 3: Auth Context Provider
Context provider for app-wide socket authentication.
// contexts/socket-auth-context.tsx
import {
createContext,
useContext,
useEffect,
useState,
useCallback,
type ReactNode,
} from "react";
import type { Socket } from "socket.io-client";
import { useSocketAuth } from "../hooks/use-socket-auth";
interface SocketAuthContextValue {
socket: Socket | null;
isAuthenticated: boolean;
isConnecting: boolean;
error: { code: string; message: string } | null;
connect: () => void;
disconnect: () => void;
}
const SocketAuthContext = createContext<SocketAuthContextValue | null>(null);
const SOCKET_AUTH_CONTEXT_ERROR =
"useSocketAuthContext must be used within SocketAuthProvider";
interface SocketAuthProviderProps {
children: ReactNode;
getToken: () => string | null;
refreshToken: () => Promise<string | null>;
autoConnect?: boolean;
}
export function SocketAuthProvider({
children,
getToken,
refreshToken,
autoConnect = false,
}: SocketAuthProviderProps): JSX.Element {
const auth = useSocketAuth({
getToken,
refreshToken,
onAuthError: (error) => {
// Handle auth error (log, display to user, etc.)
},
});
// Auto-connect when token is available
useEffect(() => {
if (autoConnect && getToken() && !auth.socket) {
auth.connect();
}
}, [autoConnect, getToken, auth]);
return (
<SocketAuthContext.Provider value={auth}>
{children}
</SocketAuthContext.Provider>
);
}
export function useSocketAuthContext(): SocketAuthContextValue {
const context = useContext(SocketAuthContext);
if (!context) {
throw new Error(SOCKET_AUTH_CONTEXT_ERROR);
}
return context;
}---
Example 4: Reconnection with Credential Refresh
Handle reconnection when token changes during disconnect.
Constants
const MAX_RECONNECT_ATTEMPTS = 10;
const AUTH_RETRY_DELAY_MS = 2000;Implementation
// lib/socket-reconnection.ts
import type { Socket } from "socket.io-client";
interface ReconnectionConfig {
socket: Socket;
getToken: () => string | null;
refreshToken: () => Promise<string | null>;
onReconnected: () => void;
onReconnectFailed: (reason: string) => void;
}
export function setupReconnectionWithAuth(
config: ReconnectionConfig,
): () => void {
const { socket, getToken, refreshToken, onReconnected, onReconnectFailed } =
config;
let reconnectAttempts = 0;
// Before each reconnection attempt, refresh the token if needed
const handleReconnectAttempt = async (attempt: number): Promise<void> => {
reconnectAttempts = attempt;
// Get current token
let token = getToken();
// Try to refresh if token is missing or might be expired
if (!token) {
token = await refreshToken();
}
if (token) {
// Update socket auth before reconnection
socket.auth = { token };
} else {
// No valid token - stop reconnection
socket.io.engine?.close();
onReconnectFailed("Could not refresh authentication token");
}
};
// Successful reconnection
const handleReconnect = (): void => {
reconnectAttempts = 0;
onReconnected();
};
// Failed to reconnect after all attempts
const handleReconnectFailed = (): void => {
onReconnectFailed(
`Failed to reconnect after ${MAX_RECONNECT_ATTEMPTS} attempts`,
);
};
// Connection error during reconnection
const handleReconnectError = async (error: Error): Promise<void> => {
// If auth error, try refreshing token
if (
error.message.includes("auth") ||
error.message.includes("unauthorized")
) {
const newToken = await refreshToken();
if (newToken) {
socket.auth = { token: newToken };
// Let Socket.IO continue with reconnection
}
}
};
// Attach handlers
socket.io.on("reconnect_attempt", handleReconnectAttempt);
socket.io.on("reconnect", handleReconnect);
socket.io.on("reconnect_failed", handleReconnectFailed);
socket.io.on("reconnect_error", handleReconnectError);
// Return cleanup
return () => {
socket.io.off("reconnect_attempt", handleReconnectAttempt);
socket.io.off("reconnect", handleReconnect);
socket.io.off("reconnect_failed", handleReconnectFailed);
socket.io.off("reconnect_error", handleReconnectError);
};
}---
Example 5: Namespace-Level Authentication
Different authentication per namespace (e.g., admin requires elevated permissions).
// lib/namespace-auth.ts
import { Manager, Socket } from "socket.io-client";
const SOCKET_URL = process.env.SOCKET_URL ?? "http://localhost:3001";
interface NamespaceAuthConfig {
userToken: string;
adminToken?: string;
}
interface AuthenticatedNamespaces {
main: Socket;
admin: Socket | null;
manager: Manager;
}
export function createAuthenticatedNamespaces(
config: NamespaceAuthConfig,
): AuthenticatedNamespaces {
const { userToken, adminToken } = config;
// Create manager with base auth
const manager = new Manager(SOCKET_URL, {
autoConnect: false,
});
// Main namespace - user-level auth
const mainSocket = manager.socket("/", {
auth: { token: userToken },
});
// Admin namespace - elevated auth (only if admin token provided)
let adminSocket: Socket | null = null;
if (adminToken) {
adminSocket = manager.socket("/admin", {
auth: { token: adminToken, elevated: true },
});
}
return {
main: mainSocket,
admin: adminSocket,
manager,
};
}
// Usage
// const { main, admin, manager } = createAuthenticatedNamespaces({
// userToken: "user-jwt-token",
// adminToken: hasAdminAccess ? "admin-jwt-token" : undefined,
// });
//
// manager.connect(); // Connect all namespaces---
Example 6: Cookie-Based Authentication
For session-based auth where cookies are automatically sent.
// lib/socket-cookie-auth.ts
import { io, Socket } from "socket.io-client";
const SOCKET_URL = process.env.SOCKET_URL ?? "http://localhost:3001";
export function createCookieAuthSocket(): Socket {
const socket = io(SOCKET_URL, {
// Enable credentials for cross-origin cookie sending
withCredentials: true,
// Transports that support cookies
transports: ["websocket", "polling"],
// No auth option needed - cookies are sent automatically
autoConnect: false,
});
return socket;
}
// Server must be configured with:
// const io = new Server(httpServer, {
// cors: {
// origin: "http://your-frontend.com",
// credentials: true
// }
// });---
Example 7: Auth State Machine
Manage complex auth states with clear transitions.
// lib/auth-state-machine.ts
import type { Socket } from "socket.io-client";
type AuthState =
| "disconnected"
| "connecting"
| "authenticating"
| "authenticated"
| "refreshing"
| "error";
type AuthEvent =
| { type: "CONNECT" }
| { type: "CONNECTED" }
| { type: "AUTH_SUCCESS" }
| { type: "AUTH_ERROR"; error: string }
| { type: "REFRESH_REQUIRED" }
| { type: "REFRESH_SUCCESS" }
| { type: "REFRESH_FAILED"; error: string }
| { type: "DISCONNECT" }
| { type: "DISCONNECTED" };
interface AuthStateContext {
error: string | null;
userId: string | null;
sessionId: string | null;
}
type AuthStateListener = (state: AuthState, context: AuthStateContext) => void;
export class AuthStateMachine {
private state: AuthState = "disconnected";
private context: AuthStateContext = {
error: null,
userId: null,
sessionId: null,
};
private listeners: Set<AuthStateListener> = new Set();
private transition(event: AuthEvent): void {
const prevState = this.state;
switch (this.state) {
case "disconnected":
if (event.type === "CONNECT") {
this.state = "connecting";
}
break;
case "connecting":
if (event.type === "CONNECTED") {
this.state = "authenticating";
} else if (
event.type === "DISCONNECT" ||
event.type === "DISCONNECTED"
) {
this.state = "disconnected";
}
break;
case "authenticating":
if (event.type === "AUTH_SUCCESS") {
this.state = "authenticated";
this.context.error = null;
} else if (event.type === "AUTH_ERROR") {
this.state = "error";
this.context.error = event.error;
} else if (event.type === "DISCONNECTED") {
this.state = "disconnected";
}
break;
case "authenticated":
if (event.type === "REFRESH_REQUIRED") {
this.state = "refreshing";
} else if (event.type === "DISCONNECTED") {
this.state = "disconnected";
} else if (event.type === "DISCONNECT") {
this.state = "disconnected";
}
break;
case "refreshing":
if (event.type === "REFRESH_SUCCESS") {
this.state = "authenticated";
} else if (event.type === "REFRESH_FAILED") {
this.state = "error";
this.context.error = event.error;
} else if (event.type === "DISCONNECTED") {
this.state = "disconnected";
}
break;
case "error":
if (event.type === "CONNECT") {
this.state = "connecting";
this.context.error = null;
} else if (event.type === "DISCONNECT") {
this.state = "disconnected";
}
break;
}
if (this.state !== prevState) {
this.notifyListeners();
}
}
send(event: AuthEvent): void {
this.transition(event);
}
getState(): AuthState {
return this.state;
}
getContext(): AuthStateContext {
return { ...this.context };
}
subscribe(listener: AuthStateListener): () => void {
this.listeners.add(listener);
// Immediately notify with current state
listener(this.state, this.context);
return () => {
this.listeners.delete(listener);
};
}
private notifyListeners(): void {
this.listeners.forEach((listener) => {
listener(this.state, this.context);
});
}
}
// Hook socket events to state machine
export function connectStateMachine(
socket: Socket,
machine: AuthStateMachine,
): () => void {
const handleConnect = (): void => machine.send({ type: "CONNECTED" });
const handleDisconnect = (): void => machine.send({ type: "DISCONNECTED" });
const handleAuthSuccess = (): void => machine.send({ type: "AUTH_SUCCESS" });
const handleAuthError = (err: { message: string }): void => {
machine.send({ type: "AUTH_ERROR", error: err.message });
};
const handleRefreshRequired = (): void => {
machine.send({ type: "REFRESH_REQUIRED" });
};
socket.on("connect", handleConnect);
socket.on("disconnect", handleDisconnect);
socket.on("auth:success", handleAuthSuccess);
socket.on("auth:error", handleAuthError);
socket.on("auth:refresh_required", handleRefreshRequired);
return () => {
socket.off("connect", handleConnect);
socket.off("disconnect", handleDisconnect);
socket.off("auth:success", handleAuthSuccess);
socket.off("auth:error", handleAuthError);
socket.off("auth:refresh_required", handleRefreshRequired);
};
}---
Anti-Pattern Examples
WRONG: Token in Query String
// WRONG - Token visible in server logs, browser history, and proxy logs
const socket = io(`http://localhost:3001?token=${token}`);WRONG: No Token Refresh Handling
// WRONG - Token expires, connection fails, no recovery
const socket = io(url, {
auth: { token: getToken() },
});
socket.connect();
// Token expires... connection lost foreverWRONG: Hardcoded Token
// WRONG - Stale token after expiration
const socket = io(url, {
auth: { token: "some-static-token" },
});CORRECT Versions
// CORRECT - Token in auth option
const socket = io(url, {
auth: { token },
});
// CORRECT - Token refresh before reconnection
socket.io.on("reconnect_attempt", async () => {
const freshToken = await refreshToken();
socket.auth = { token: freshToken };
});
// CORRECT - Dynamic token from storage/state
const socket = io(url, {
auth: (cb) => {
cb({ token: getToken() });
}, // Function called on each connection
});Socket.IO Core Examples
Complete, copy-paste ready examples for Socket.IO client fundamentals. All examples use TypeScript with proper typing.
---
Example 1: Type-Safe Socket Factory
Create a typed socket instance with proper configuration.
Constants
// Use your framework's env variable convention
const SOCKET_URL = process.env.SOCKET_URL ?? "http://localhost:3001";
const RECONNECTION_ATTEMPTS = 10;
const RECONNECTION_DELAY_MS = 1000;
const RECONNECTION_DELAY_MAX_MS = 5000;
const CONNECTION_TIMEOUT_MS = 20000;Event Types
// types/socket-events.ts
export interface ServerToClientEvents {
"chat:message": (message: Message) => void;
"chat:typing": (data: TypingIndicator) => void;
"user:status": (data: UserStatus) => void;
notification: (notification: Notification) => void;
error: (error: ServerError) => void;
}
export interface ClientToServerEvents {
"chat:send": (
content: string,
roomId: string,
ack: (result: SendResult) => void,
) => void;
"chat:typing": (roomId: string) => void;
"room:join": (roomId: string, ack: (result: JoinResult) => void) => void;
"room:leave": (roomId: string) => void;
}
export interface Message {
id: string;
content: string;
senderId: string;
senderName: string;
roomId: string;
timestamp: number;
}
export interface TypingIndicator {
userId: string;
username: string;
roomId: string;
}
export interface UserStatus {
userId: string;
status: "online" | "offline" | "away";
}
export interface Notification {
id: string;
type: "info" | "warning" | "error";
message: string;
}
export interface ServerError {
code: string;
message: string;
}
export interface SendResult {
success: boolean;
messageId?: string;
error?: string;
}
export interface JoinResult {
success: boolean;
roomName?: string;
members?: string[];
error?: string;
}Socket Factory
// lib/socket.ts
import { io, Socket } from "socket.io-client";
import type {
ServerToClientEvents,
ClientToServerEvents,
} from "../types/socket-events";
export type TypedSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
let socket: TypedSocket | null = null;
export function createSocket(authToken: string): TypedSocket {
// Disconnect existing socket if any
if (socket?.connected) {
socket.disconnect();
}
socket = io(SOCKET_URL, {
auth: { token: authToken },
autoConnect: false,
reconnection: true,
reconnectionAttempts: RECONNECTION_ATTEMPTS,
reconnectionDelay: RECONNECTION_DELAY_MS,
reconnectionDelayMax: RECONNECTION_DELAY_MAX_MS,
timeout: CONNECTION_TIMEOUT_MS,
transports: ["websocket", "polling"],
});
return socket;
}
export function getSocket(): TypedSocket | null {
return socket;
}
export function disconnectSocket(): void {
socket?.disconnect();
socket = null;
}
export { SOCKET_URL, CONNECTION_TIMEOUT_MS };---
Example 2: React Integration Hook
Custom hook for managing Socket.IO connection in React components.
Constants
const SOCKET_CONTEXT_ERROR = "useSocket must be used within SocketProvider";Connection Hook
// hooks/use-socket-connection.ts
import { useEffect, useState, useCallback } from "react";
import type { TypedSocket } from "../lib/socket";
import { createSocket, disconnectSocket, getSocket } from "../lib/socket";
interface ConnectionState {
isConnected: boolean;
isReconnecting: boolean;
error: Error | null;
}
interface UseSocketConnectionOptions {
token: string;
autoConnect?: boolean;
onConnect?: () => void;
onDisconnect?: (reason: string) => void;
onError?: (error: Error) => void;
}
interface UseSocketConnectionResult extends ConnectionState {
socket: TypedSocket | null;
connect: () => void;
disconnect: () => void;
}
export function useSocketConnection(
options: UseSocketConnectionOptions,
): UseSocketConnectionResult {
const {
token,
autoConnect = true,
onConnect,
onDisconnect,
onError,
} = options;
const [state, setState] = useState<ConnectionState>({
isConnected: false,
isReconnecting: false,
error: null,
});
const [socket, setSocket] = useState<TypedSocket | null>(null);
// Initialize socket
useEffect(() => {
if (!token) return;
const newSocket = createSocket(token);
setSocket(newSocket);
// Connection event handlers
const handleConnect = (): void => {
setState((prev) => ({
...prev,
isConnected: true,
isReconnecting: false,
error: null,
}));
onConnect?.();
};
const handleDisconnect = (reason: string): void => {
setState((prev) => ({
...prev,
isConnected: false,
isReconnecting: newSocket.active,
}));
onDisconnect?.(reason);
};
const handleConnectError = (error: Error): void => {
setState((prev) => ({
...prev,
isConnected: false,
error,
}));
onError?.(error);
};
const handleReconnectAttempt = (): void => {
setState((prev) => ({
...prev,
isReconnecting: true,
}));
};
// Attach listeners
newSocket.on("connect", handleConnect);
newSocket.on("disconnect", handleDisconnect);
newSocket.on("connect_error", handleConnectError);
newSocket.io.on("reconnect_attempt", handleReconnectAttempt);
// Auto connect if enabled
if (autoConnect) {
newSocket.connect();
}
// Cleanup
return () => {
newSocket.off("connect", handleConnect);
newSocket.off("disconnect", handleDisconnect);
newSocket.off("connect_error", handleConnectError);
newSocket.io.off("reconnect_attempt", handleReconnectAttempt);
disconnectSocket();
};
}, [token, autoConnect, onConnect, onDisconnect, onError]);
const connect = useCallback(() => {
socket?.connect();
}, [socket]);
const disconnect = useCallback(() => {
socket?.disconnect();
}, [socket]);
return {
socket,
isConnected: state.isConnected,
isReconnecting: state.isReconnecting,
error: state.error,
connect,
disconnect,
};
}---
Example 3: Event Listener Hook
Reusable hook for subscribing to Socket.IO events with automatic cleanup.
// hooks/use-socket-event.ts
import { useEffect, useRef, useCallback } from "react";
import type { TypedSocket } from "../lib/socket";
import type { ServerToClientEvents } from "../types/socket-events";
type EventName = keyof ServerToClientEvents;
type EventHandler<E extends EventName> = ServerToClientEvents[E];
export function useSocketEvent<E extends EventName>(
socket: TypedSocket | null,
event: E,
handler: EventHandler<E>,
): void {
// Use ref to avoid re-subscribing on handler changes
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
if (!socket) return;
// Wrapper that uses current handler ref
const eventHandler = ((...args: unknown[]) => {
(handlerRef.current as (...args: unknown[]) => void)(...args);
}) as EventHandler<E>;
socket.on(event, eventHandler);
return () => {
socket.off(event, eventHandler);
};
}, [socket, event]);
}
// Usage example:
// useSocketEvent(socket, "chat:message", (message) => {
// addMessage(message); // Type-safe: message is Message
// });---
Example 4: Emit with Acknowledgment Hook
Hook for sending messages with acknowledgment handling.
Constants
const EMIT_TIMEOUT_MS = 5000;Implementation
// hooks/use-socket-emit.ts
import { useCallback, useState } from "react";
import type { TypedSocket } from "../lib/socket";
import type { ClientToServerEvents } from "../types/socket-events";
interface EmitState<T> {
isLoading: boolean;
error: Error | null;
data: T | null;
}
type EventName = keyof ClientToServerEvents;
export function useSocketEmit<T>(socket: TypedSocket | null) {
const [state, setState] = useState<EmitState<T>>({
isLoading: false,
error: null,
data: null,
});
const emit = useCallback(
async (event: EventName, ...args: unknown[]): Promise<T | null> => {
if (!socket?.connected) {
const error = new Error("Socket not connected");
setState({ isLoading: false, error, data: null });
return null;
}
setState({ isLoading: true, error: null, data: null });
try {
const response = await socket
.timeout(EMIT_TIMEOUT_MS)
.emitWithAck(event, ...args);
setState({ isLoading: false, error: null, data: response as T });
return response as T;
} catch (error) {
const err = error instanceof Error ? error : new Error("Emit failed");
setState({ isLoading: false, error: err, data: null });
return null;
}
},
[socket],
);
const reset = useCallback(() => {
setState({ isLoading: false, error: null, data: null });
}, []);
return { ...state, emit, reset };
}---
Example 5: Connection Status Component
Display connection state to users with reconnection feedback.
// components/connection-status.tsx
import { useSocketConnection } from "../hooks/use-socket-connection";
interface ConnectionStatusProps {
className?: string;
}
export function ConnectionStatus({
className,
}: ConnectionStatusProps): JSX.Element {
const { isConnected, isReconnecting, error } = useSocketConnection({
token: "...", // Get from auth context
});
if (error) {
return (
<div className={className} data-status="error">
<span>Connection failed</span>
<span>{error.message}</span>
</div>
);
}
if (isReconnecting) {
return (
<div className={className} data-status="reconnecting">
<span>Reconnecting...</span>
</div>
);
}
return (
<div className={className} data-status={isConnected ? "connected" : "disconnected"}>
<span>{isConnected ? "Connected" : "Disconnected"}</span>
</div>
);
}---
Example 6: Message Queue for Offline Support
Queue messages when disconnected and flush on reconnection.
Constants
const MAX_QUEUE_SIZE = 100;Implementation
// lib/message-queue.ts
interface QueuedMessage {
event: string;
data: unknown;
timestamp: number;
}
export class MessageQueue {
private queue: QueuedMessage[] = [];
private maxSize: number;
constructor(maxSize: number = MAX_QUEUE_SIZE) {
this.maxSize = maxSize;
}
enqueue(event: string, data: unknown): void {
if (this.queue.length >= this.maxSize) {
// Remove oldest message
this.queue.shift();
}
this.queue.push({
event,
data,
timestamp: Date.now(),
});
}
flush(emitFn: (event: string, data: unknown) => void): number {
const count = this.queue.length;
while (this.queue.length > 0) {
const message = this.queue.shift();
if (message) {
emitFn(message.event, message.data);
}
}
return count;
}
clear(): void {
this.queue = [];
}
get size(): number {
return this.queue.length;
}
}
// Usage with socket
// const queue = new MessageQueue();
//
// function send(event: string, data: unknown) {
// if (socket.connected) {
// socket.emit(event, data);
// } else {
// queue.enqueue(event, data);
// }
// }
//
// socket.on("connect", () => {
// queue.flush((event, data) => socket.emit(event, data));
// });---
Example 7: Typing Indicator
Implement typing indicators with debouncing.
Constants
const TYPING_DEBOUNCE_MS = 300;
const TYPING_TIMEOUT_MS = 2000;Implementation
// hooks/use-typing-indicator.ts
import { useCallback, useRef, useEffect } from "react";
import type { TypedSocket } from "../lib/socket";
interface UseTypingIndicatorOptions {
socket: TypedSocket | null;
roomId: string;
}
export function useTypingIndicator({
socket,
roomId,
}: UseTypingIndicatorOptions) {
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const isTypingRef = useRef(false);
// Cleanup on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
const startTyping = useCallback(() => {
if (!socket?.connected) return;
// Emit typing:start only if not already typing
if (!isTypingRef.current) {
socket.emit("chat:typing", roomId);
isTypingRef.current = true;
}
// Clear existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Set timeout to stop typing indicator
timeoutRef.current = setTimeout(() => {
isTypingRef.current = false;
}, TYPING_TIMEOUT_MS);
}, [socket, roomId]);
const stopTyping = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
isTypingRef.current = false;
}, []);
return { startTyping, stopTyping };
}---
Example 8: Volatile Events
Use volatile events for data that can be safely dropped (e.g., cursor positions, live updates).
// lib/volatile-events.ts
import type { Socket } from "socket.io-client";
interface CursorPosition {
x: number;
y: number;
userId: string;
}
// Volatile: OK if some updates are dropped during congestion
export function emitCursorPosition(
socket: Socket,
position: CursorPosition,
): void {
socket.volatile.emit("cursor:move", position);
}
// Regular: Every update must be delivered
export function emitImportantUpdate(socket: Socket, data: unknown): void {
socket.emit("update:important", data);
}---
Example 9: Multiplexing with Manager
Share a single connection across multiple namespace sockets.
// lib/socket-manager.ts
import { Manager, Socket } from "socket.io-client";
const MANAGER_RECONNECTION_DELAY_MS = 1000;
const MANAGER_RECONNECTION_DELAY_MAX_MS = 5000;
// Single Manager for connection sharing
const manager = new Manager("http://localhost:3001", {
autoConnect: false,
reconnectionDelay: MANAGER_RECONNECTION_DELAY_MS,
reconnectionDelayMax: MANAGER_RECONNECTION_DELAY_MAX_MS,
});
// Multiple namespace sockets share one connection
export const chatSocket = manager.socket("/chat");
export const notificationSocket = manager.socket("/notifications");
export const adminSocket = manager.socket("/admin");
export function connectAll(token: string): void {
// Set auth for all sockets
manager.opts.auth = { token };
// Connect the manager (connects all sockets)
manager.connect();
}
export function disconnectAll(): void {
manager.disconnect();
}
// Individual namespace control
export function connectNamespace(socket: Socket): void {
socket.connect();
}
export function disconnectNamespace(socket: Socket): void {
socket.disconnect();
}---
Anti-Pattern Examples
WRONG: Token in Query String
// WRONG - Token visible in server logs and browser history
const socket = io(`http://localhost:3001?token=${token}`);WRONG: No Event Listener Cleanup
// WRONG - Memory leak, duplicate handlers on re-render
useEffect(() => {
socket.on("message", handleMessage);
// Missing cleanup!
}, []);WRONG: Magic Numbers
// WRONG - What do these numbers mean?
const socket = io(url, {
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 10000,
});WRONG: No readyState Check
// WRONG - Fails silently if not connected
function sendMessage(data: unknown) {
socket.emit("message", data);
}CORRECT Versions
// CORRECT - Token in auth object
const socket = io(url, {
auth: { token },
});
// CORRECT - Cleanup listeners
useEffect(() => {
socket.on("message", handleMessage);
return () => {
socket.off("message", handleMessage);
};
}, []);
// CORRECT - Named constants
const socket = io(url, {
reconnectionDelay: RECONNECTION_DELAY_MS,
reconnectionDelayMax: RECONNECTION_DELAY_MAX_MS,
timeout: CONNECTION_TIMEOUT_MS,
});
// CORRECT - Check connection status
function sendMessage(data: unknown) {
if (socket.connected) {
socket.emit("message", data);
} else {
messageQueue.enqueue(data);
}
}Socket.IO Rooms and Namespaces Examples
Patterns for rooms (server-side grouping) and namespaces (protocol-level separation) in Socket.IO v4.x.
---
Understanding Rooms vs Namespaces
Rooms: Server-side grouping mechanism. Clients are unaware of rooms - they're purely a server concept for organizing which sockets receive which broadcasts.
Namespaces: Protocol-level separation. Clients explicitly connect to namespaces. Each namespace is a distinct communication channel that can have its own event handlers and middleware.
| Feature | Rooms | Namespaces |
|---|---|---|
| Client awareness | No | Yes |
| Join/Leave | Server-side only | Client connects to |
| Use case | Targeted broadcasting | Feature separation |
| Example | Chat rooms | /chat, /admin, /notifications |
---
Room Patterns (Client Perspective)
Example 1: Joining and Leaving Rooms
Rooms are joined server-side, but clients can request to join/leave.
Constants
const JOIN_ROOM_TIMEOUT_MS = 5000;Types
// types/room-events.ts
interface RoomJoinResult {
success: boolean;
roomId: string;
roomName?: string;
members?: RoomMember[];
error?: string;
}
interface RoomLeaveResult {
success: boolean;
roomId: string;
}
interface RoomMember {
userId: string;
username: string;
joinedAt: number;
}
interface RoomUpdate {
roomId: string;
type: "member_joined" | "member_left";
member: RoomMember;
memberCount: number;
}
// Client-to-server events for rooms
interface RoomClientEvents {
"room:join": (
roomId: string,
callback: (result: RoomJoinResult) => void,
) => void;
"room:leave": (
roomId: string,
callback: (result: RoomLeaveResult) => void,
) => void;
}
// Server-to-client events for rooms
interface RoomServerEvents {
"room:joined": (data: RoomJoinResult) => void;
"room:left": (data: { roomId: string }) => void;
"room:update": (update: RoomUpdate) => void;
"room:message": (message: RoomMessage) => void;
}
interface RoomMessage {
id: string;
roomId: string;
senderId: string;
senderName: string;
content: string;
timestamp: number;
}
export type {
RoomJoinResult,
RoomLeaveResult,
RoomMember,
RoomUpdate,
RoomMessage,
RoomClientEvents,
RoomServerEvents,
};Implementation
// lib/room-manager.ts
import type { Socket } from "socket.io-client";
import type {
RoomJoinResult,
RoomLeaveResult,
RoomMember,
RoomUpdate,
RoomMessage,
} from "../types/room-events";
interface RoomState {
currentRooms: Map<string, RoomMember[]>;
}
type RoomEventHandler = {
onJoined?: (result: RoomJoinResult) => void;
onLeft?: (roomId: string) => void;
onUpdate?: (update: RoomUpdate) => void;
onMessage?: (message: RoomMessage) => void;
};
export class RoomManager {
private socket: Socket;
private state: RoomState = {
currentRooms: new Map(),
};
constructor(socket: Socket) {
this.socket = socket;
}
async joinRoom(roomId: string): Promise<RoomJoinResult> {
if (!this.socket.connected) {
return {
success: false,
roomId,
error: "Not connected",
};
}
try {
const result = await this.socket
.timeout(JOIN_ROOM_TIMEOUT_MS)
.emitWithAck("room:join", roomId);
if (result.success && result.members) {
this.state.currentRooms.set(roomId, result.members);
}
return result as RoomJoinResult;
} catch (error) {
return {
success: false,
roomId,
error: error instanceof Error ? error.message : "Join failed",
};
}
}
async leaveRoom(roomId: string): Promise<RoomLeaveResult> {
if (!this.socket.connected) {
return { success: false, roomId };
}
try {
const result = await this.socket
.timeout(JOIN_ROOM_TIMEOUT_MS)
.emitWithAck("room:leave", roomId);
if (result.success) {
this.state.currentRooms.delete(roomId);
}
return result as RoomLeaveResult;
} catch {
return { success: false, roomId };
}
}
leaveAllRooms(): void {
for (const roomId of this.state.currentRooms.keys()) {
this.socket.emit("room:leave", roomId);
}
this.state.currentRooms.clear();
}
isInRoom(roomId: string): boolean {
return this.state.currentRooms.has(roomId);
}
getRoomMembers(roomId: string): RoomMember[] {
return this.state.currentRooms.get(roomId) ?? [];
}
getCurrentRooms(): string[] {
return Array.from(this.state.currentRooms.keys());
}
setupEventHandlers(handlers: RoomEventHandler): () => void {
const handleUpdate = (update: RoomUpdate): void => {
// Update local member list
const members = this.state.currentRooms.get(update.roomId);
if (members) {
if (update.type === "member_joined") {
members.push(update.member);
} else {
const index = members.findIndex(
(m) => m.userId === update.member.userId,
);
if (index !== -1) {
members.splice(index, 1);
}
}
}
handlers.onUpdate?.(update);
};
const handleMessage = (message: RoomMessage): void => {
handlers.onMessage?.(message);
};
this.socket.on("room:update", handleUpdate);
this.socket.on("room:message", handleMessage);
return () => {
this.socket.off("room:update", handleUpdate);
this.socket.off("room:message", handleMessage);
};
}
}---
Example 2: Room Hook for React
React hook for managing room membership.
// hooks/use-room.ts
import { useState, useEffect, useCallback, useRef } from "react";
import type { Socket } from "socket.io-client";
import type {
RoomJoinResult,
RoomMember,
RoomUpdate,
RoomMessage,
} from "../types/room-events";
import { RoomManager } from "../lib/room-manager";
interface UseRoomOptions {
socket: Socket | null;
roomId: string;
autoJoin?: boolean;
onMessage?: (message: RoomMessage) => void;
onMemberUpdate?: (update: RoomUpdate) => void;
}
interface UseRoomResult {
isJoined: boolean;
isJoining: boolean;
members: RoomMember[];
error: string | null;
join: () => Promise<void>;
leave: () => Promise<void>;
sendMessage: (content: string) => void;
}
export function useRoom(options: UseRoomOptions): UseRoomResult {
const {
socket,
roomId,
autoJoin = false,
onMessage,
onMemberUpdate,
} = options;
const [isJoined, setIsJoined] = useState(false);
const [isJoining, setIsJoining] = useState(false);
const [members, setMembers] = useState<RoomMember[]>([]);
const [error, setError] = useState<string | null>(null);
const managerRef = useRef<RoomManager | null>(null);
// Initialize room manager
useEffect(() => {
if (!socket) return;
managerRef.current = new RoomManager(socket);
const cleanup = managerRef.current.setupEventHandlers({
onUpdate: (update) => {
if (update.roomId === roomId) {
setMembers(managerRef.current?.getRoomMembers(roomId) ?? []);
onMemberUpdate?.(update);
}
},
onMessage: (message) => {
if (message.roomId === roomId) {
onMessage?.(message);
}
},
});
return () => {
cleanup();
managerRef.current = null;
};
}, [socket, roomId, onMessage, onMemberUpdate]);
// Auto-join if enabled
useEffect(() => {
if (autoJoin && socket?.connected && managerRef.current && !isJoined) {
join();
}
}, [autoJoin, socket?.connected, isJoined]);
// Leave room on unmount or roomId change
useEffect(() => {
return () => {
if (isJoined && managerRef.current) {
managerRef.current.leaveRoom(roomId);
}
};
}, [roomId, isJoined]);
const join = useCallback(async () => {
if (!managerRef.current || isJoined || isJoining) return;
setIsJoining(true);
setError(null);
const result = await managerRef.current.joinRoom(roomId);
setIsJoining(false);
if (result.success) {
setIsJoined(true);
setMembers(result.members ?? []);
} else {
setError(result.error ?? "Failed to join room");
}
}, [roomId, isJoined, isJoining]);
const leave = useCallback(async () => {
if (!managerRef.current || !isJoined) return;
await managerRef.current.leaveRoom(roomId);
setIsJoined(false);
setMembers([]);
}, [roomId, isJoined]);
const sendMessage = useCallback(
(content: string) => {
if (!socket?.connected || !isJoined) return;
socket.emit("room:message", { roomId, content });
},
[socket, roomId, isJoined],
);
return {
isJoined,
isJoining,
members,
error,
join,
leave,
sendMessage,
};
}---
Example 3: Multi-Room Chat Component
Component that supports joining multiple rooms simultaneously.
// components/multi-room-chat.tsx
import { useState, useCallback } from "react";
import type { Socket } from "socket.io-client";
import type { RoomMessage } from "../types/room-events";
import { useRoom } from "../hooks/use-room";
interface MultiRoomChatProps {
socket: Socket | null;
availableRooms: Array<{ id: string; name: string }>;
className?: string;
}
export function MultiRoomChat({
socket,
availableRooms,
className,
}: MultiRoomChatProps): JSX.Element {
const [activeRoomId, setActiveRoomId] = useState<string | null>(null);
const [messages, setMessages] = useState<Map<string, RoomMessage[]>>(
new Map()
);
const handleMessage = useCallback((message: RoomMessage) => {
setMessages((prev) => {
const next = new Map(prev);
const roomMessages = next.get(message.roomId) ?? [];
next.set(message.roomId, [...roomMessages, message]);
return next;
});
}, []);
const activeRoom = useRoom({
socket,
roomId: activeRoomId ?? "",
autoJoin: true,
onMessage: handleMessage,
});
const handleRoomSelect = useCallback(
async (roomId: string) => {
if (activeRoomId) {
await activeRoom.leave();
}
setActiveRoomId(roomId);
},
[activeRoomId, activeRoom]
);
return (
<div className={className}>
<div data-role="room-list">
{availableRooms.map((room) => (
<button
key={room.id}
data-active={room.id === activeRoomId}
onClick={() => handleRoomSelect(room.id)}
>
{room.name}
</button>
))}
</div>
{activeRoomId && (
<div data-role="chat-area">
<div data-role="members">
{activeRoom.members.map((member) => (
<span key={member.userId}>{member.username}</span>
))}
</div>
<div data-role="messages">
{(messages.get(activeRoomId) ?? []).map((msg) => (
<div key={msg.id}>
<strong>{msg.senderName}:</strong> {msg.content}
</div>
))}
</div>
<MessageInput
onSend={activeRoom.sendMessage}
disabled={!activeRoom.isJoined}
/>
</div>
)}
</div>
);
}
interface MessageInputProps {
onSend: (content: string) => void;
disabled: boolean;
}
function MessageInput({ onSend, disabled }: MessageInputProps): JSX.Element {
const [value, setValue] = useState("");
const handleSubmit = (e: React.FormEvent): void => {
e.preventDefault();
if (value.trim() && !disabled) {
onSend(value.trim());
setValue("");
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
disabled={disabled}
placeholder="Type a message..."
/>
<button type="submit" disabled={disabled || !value.trim()}>
Send
</button>
</form>
);
}---
Namespace Patterns
Example 4: Multiple Namespaces
Connect to multiple namespaces for feature separation.
Constants
const NAMESPACE_RECONNECTION_DELAY_MS = 1000;
const NAMESPACE_RECONNECTION_DELAY_MAX_MS = 5000;Types
// types/namespace-events.ts
// Chat namespace events
interface ChatServerEvents {
message: (message: ChatMessage) => void;
typing: (data: { userId: string; username: string }) => void;
}
interface ChatClientEvents {
send: (content: string, roomId: string) => void;
typing: (roomId: string) => void;
}
// Notification namespace events
interface NotificationServerEvents {
notification: (notification: AppNotification) => void;
"notification:read": (notificationId: string) => void;
}
interface NotificationClientEvents {
markRead: (notificationId: string) => void;
markAllRead: () => void;
}
// Admin namespace events
interface AdminServerEvents {
stats: (stats: SystemStats) => void;
alert: (alert: SystemAlert) => void;
}
interface AdminClientEvents {
requestStats: () => void;
clearAlert: (alertId: string) => void;
}
interface ChatMessage {
id: string;
content: string;
senderId: string;
roomId: string;
timestamp: number;
}
interface AppNotification {
id: string;
type: "info" | "warning" | "success" | "error";
title: string;
message: string;
read: boolean;
createdAt: number;
}
interface SystemStats {
connectedUsers: number;
activeRooms: number;
messageRate: number;
}
interface SystemAlert {
id: string;
severity: "low" | "medium" | "high" | "critical";
message: string;
timestamp: number;
}
export type {
ChatServerEvents,
ChatClientEvents,
NotificationServerEvents,
NotificationClientEvents,
AdminServerEvents,
AdminClientEvents,
ChatMessage,
AppNotification,
SystemStats,
SystemAlert,
};Implementation
// lib/namespace-sockets.ts
import { Manager, Socket } from "socket.io-client";
import type {
ChatServerEvents,
ChatClientEvents,
NotificationServerEvents,
NotificationClientEvents,
AdminServerEvents,
AdminClientEvents,
} from "../types/namespace-events";
const SOCKET_URL = process.env.SOCKET_URL ?? "http://localhost:3001";
// Typed socket types
type ChatSocket = Socket<ChatServerEvents, ChatClientEvents>;
type NotificationSocket = Socket<
NotificationServerEvents,
NotificationClientEvents
>;
type AdminSocket = Socket<AdminServerEvents, AdminClientEvents>;
// Single Manager - shares one connection
let manager: Manager | null = null;
// Namespace sockets
let chatSocket: ChatSocket | null = null;
let notificationSocket: NotificationSocket | null = null;
let adminSocket: AdminSocket | null = null;
interface NamespaceOptions {
token: string;
onConnectionError?: (namespace: string, error: Error) => void;
}
export function initializeNamespaces(options: NamespaceOptions): void {
const { token, onConnectionError } = options;
// Create manager if not exists
if (!manager) {
manager = new Manager(SOCKET_URL, {
autoConnect: false,
reconnectionDelay: NAMESPACE_RECONNECTION_DELAY_MS,
reconnectionDelayMax: NAMESPACE_RECONNECTION_DELAY_MAX_MS,
});
}
// Set auth
manager.opts.auth = { token };
// Create namespace sockets
chatSocket = manager.socket("/chat") as ChatSocket;
notificationSocket = manager.socket("/notifications") as NotificationSocket;
adminSocket = manager.socket("/admin") as AdminSocket;
// Setup error handlers
const sockets = [
{ socket: chatSocket, name: "chat" },
{ socket: notificationSocket, name: "notifications" },
{ socket: adminSocket, name: "admin" },
];
sockets.forEach(({ socket, name }) => {
socket.on("connect_error", (error) => {
// Handle namespace connection error (log, display to user, etc.)
onConnectionError?.(name, error);
});
});
}
export function connectNamespace(
namespace: "chat" | "notifications" | "admin",
): void {
const socket = getNamespaceSocket(namespace);
socket?.connect();
}
export function disconnectNamespace(
namespace: "chat" | "notifications" | "admin",
): void {
const socket = getNamespaceSocket(namespace);
socket?.disconnect();
}
export function connectAllNamespaces(): void {
manager?.connect();
}
export function disconnectAllNamespaces(): void {
manager?.disconnect();
}
function getNamespaceSocket(
namespace: "chat" | "notifications" | "admin",
): Socket | null {
switch (namespace) {
case "chat":
return chatSocket;
case "notifications":
return notificationSocket;
case "admin":
return adminSocket;
default:
return null;
}
}
export function getChatSocket(): ChatSocket {
if (!chatSocket) throw new Error("Chat socket not initialized");
return chatSocket;
}
export function getNotificationSocket(): NotificationSocket {
if (!notificationSocket)
throw new Error("Notification socket not initialized");
return notificationSocket;
}
export function getAdminSocket(): AdminSocket {
if (!adminSocket) throw new Error("Admin socket not initialized");
return adminSocket;
}
export function cleanupNamespaces(): void {
chatSocket?.disconnect();
notificationSocket?.disconnect();
adminSocket?.disconnect();
manager?.disconnect();
chatSocket = null;
notificationSocket = null;
adminSocket = null;
manager = null;
}---
Example 5: Namespace Hook
React hook for namespace-specific connections.
// hooks/use-namespace.ts
import { useEffect, useState, useCallback } from "react";
import type { Socket } from "socket.io-client";
import { Manager } from "socket.io-client";
const SOCKET_URL = process.env.SOCKET_URL ?? "http://localhost:3001";
interface UseNamespaceOptions {
namespace: string;
token: string;
autoConnect?: boolean;
}
interface UseNamespaceResult<S, C> {
socket: Socket<S, C> | null;
isConnected: boolean;
connect: () => void;
disconnect: () => void;
}
// Singleton manager for connection sharing
let sharedManager: Manager | null = null;
function getManager(token: string): Manager {
if (!sharedManager) {
sharedManager = new Manager(SOCKET_URL, {
autoConnect: false,
});
}
sharedManager.opts.auth = { token };
return sharedManager;
}
export function useNamespace<
ServerEvents = Record<string, unknown>,
ClientEvents = Record<string, unknown>,
>(
options: UseNamespaceOptions,
): UseNamespaceResult<ServerEvents, ClientEvents> {
const { namespace, token, autoConnect = true } = options;
const [socket, setSocket] = useState<Socket<
ServerEvents,
ClientEvents
> | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
if (!token) return;
const manager = getManager(token);
const nsSocket = manager.socket(namespace) as Socket<
ServerEvents,
ClientEvents
>;
setSocket(nsSocket);
const handleConnect = (): void => setIsConnected(true);
const handleDisconnect = (): void => setIsConnected(false);
nsSocket.on("connect", handleConnect);
nsSocket.on("disconnect", handleDisconnect);
if (autoConnect) {
nsSocket.connect();
}
return () => {
nsSocket.off("connect", handleConnect);
nsSocket.off("disconnect", handleDisconnect);
nsSocket.disconnect();
};
}, [namespace, token, autoConnect]);
const connect = useCallback(() => socket?.connect(), [socket]);
const disconnect = useCallback(() => socket?.disconnect(), [socket]);
return { socket, isConnected, connect, disconnect };
}---
Example 6: Conditional Namespace Access
Connect to admin namespace only for authorized users.
// hooks/use-admin-namespace.ts
import { useEffect, useState } from "react";
import type { Socket } from "socket.io-client";
import type {
AdminServerEvents,
AdminClientEvents,
SystemStats,
SystemAlert,
} from "../types/namespace-events";
import { useNamespace } from "./use-namespace";
interface UseAdminNamespaceOptions {
token: string;
isAdmin: boolean;
onAlert?: (alert: SystemAlert) => void;
}
interface UseAdminNamespaceResult {
isConnected: boolean;
stats: SystemStats | null;
requestStats: () => void;
}
const INITIAL_STATS: SystemStats | null = null;
export function useAdminNamespace(
options: UseAdminNamespaceOptions,
): UseAdminNamespaceResult {
const { token, isAdmin, onAlert } = options;
const [stats, setStats] = useState<SystemStats | null>(INITIAL_STATS);
// Only connect if user is admin
const { socket, isConnected } = useNamespace<
AdminServerEvents,
AdminClientEvents
>({
namespace: "/admin",
token,
autoConnect: isAdmin,
});
// Listen for stats updates
useEffect(() => {
if (!socket) return;
const handleStats = (data: SystemStats): void => {
setStats(data);
};
const handleAlert = (alert: SystemAlert): void => {
onAlert?.(alert);
};
socket.on("stats", handleStats);
socket.on("alert", handleAlert);
return () => {
socket.off("stats", handleStats);
socket.off("alert", handleAlert);
};
}, [socket, onAlert]);
const requestStats = (): void => {
socket?.emit("requestStats");
};
return {
isConnected: isAdmin && isConnected,
stats,
requestStats,
};
}---
Room + Namespace Combined
Example 7: Chat Application with Rooms per Namespace
// lib/chat-namespaces.ts
import type { Socket } from "socket.io-client";
import type {
ChatServerEvents,
ChatClientEvents,
ChatMessage,
} from "../types/namespace-events";
interface ChatRoom {
id: string;
name: string;
memberCount: number;
}
interface ChatNamespaceState {
rooms: ChatRoom[];
currentRoomId: string | null;
messages: Map<string, ChatMessage[]>;
}
export class ChatNamespaceManager {
private socket: Socket<ChatServerEvents, ChatClientEvents>;
private state: ChatNamespaceState = {
rooms: [],
currentRoomId: null,
messages: new Map(),
};
constructor(socket: Socket<ChatServerEvents, ChatClientEvents>) {
this.socket = socket;
this.setupListeners();
}
private setupListeners(): void {
this.socket.on("message", (message) => {
const messages = this.state.messages.get(message.roomId) ?? [];
messages.push(message);
this.state.messages.set(message.roomId, messages);
});
}
async joinRoom(roomId: string): Promise<boolean> {
return new Promise((resolve) => {
this.socket.emit("room:join", roomId, (result) => {
if (result.success) {
this.state.currentRoomId = roomId;
}
resolve(result.success);
});
});
}
sendMessage(content: string): void {
if (!this.state.currentRoomId) return;
this.socket.emit("send", content, this.state.currentRoomId);
}
getMessages(roomId: string): ChatMessage[] {
return this.state.messages.get(roomId) ?? [];
}
getCurrentRoom(): string | null {
return this.state.currentRoomId;
}
cleanup(): void {
this.socket.off("message");
}
}# yaml-language-server: $schema=https://raw.githubusercontent.com/agents-inc/cli/main/src/schemas/metadata.schema.json
category: web-realtime
slug: socket-io
domain: web
author: "@vince"
displayName: Socket.IO
cliDescription: Bidirectional real-time communication
usageGuidance: Use when implementing Socket.IO client connections, rooms, or namespaces.
Socket.IO Reference
Decision frameworks, quick reference tables, and checklists for Socket.IO. See SKILL.md for core concepts and red flags, examples/ for code examples.
---
Decision Framework
When to Use Socket.IO vs Native WebSocket
Need real-time bidirectional communication?
├─ YES → Need rooms/namespaces for broadcast grouping?
│ ├─ YES → Socket.IO ✓
│ └─ NO → Need automatic reconnection out-of-the-box?
│ ├─ YES → Socket.IO ✓
│ └─ NO → Need acknowledgments (delivery confirmation)?
│ ├─ YES → Socket.IO ✓
│ └─ NO → Need to work in restrictive networks (fallback transports)?
│ ├─ YES → Socket.IO ✓
│ └─ NO → Native WebSocket (simpler, smaller bundle)
└─ NO → Use HTTP REST or Server-Sent EventsNamespace vs Room Decision
Need to separate communication channels?
├─ Is it a distinct feature area (chat, admin, notifications)?
│ └─ YES → Use Namespaces
│ - Clients explicitly connect
│ - Can have different middleware/auth
│ - Example: /chat, /admin, /game
├─ Is it for grouping users within a feature?
│ └─ YES → Use Rooms (within a namespace)
│ - Server-side only
│ - For targeted broadcasting
│ - Example: chat rooms, game lobbies
└─ Single unified communication
└─ Use default namespace ("/")Authentication Strategy
Implementing Socket.IO authentication?
├─ Need to pass token on initial connection?
│ └─ YES → Use `auth` option in io()
│ - Token sent in handshake
│ - Not visible in URL/logs
│ - Server validates in middleware
├─ Using session-based auth with cookies?
│ └─ YES → Set `withCredentials: true`
│ - Cookies sent automatically
│ - Server must allow credentials in CORS
├─ Need to authenticate per-namespace?
│ └─ YES → Use namespace middleware
│ - Different auth per namespace
│ - Example: basic auth for /chat, elevated for /admin
└─ NEVER put tokens in query strings (security risk)Connection State Recovery Decision
Implementing reconnection handling?
├─ Using Socket.IO v4.6.0+?
│ ├─ YES → Check socket.recovered after connect
│ │ ├─ true → Missed events delivered automatically
│ │ └─ false → Need full state refresh
│ └─ NO → Always do full state refresh on reconnect
├─ Have long-running sessions?
│ └─ YES → Implement message queuing
│ - Queue during disconnect
│ - Flush on reconnect
└─ Need exactly-once delivery?
└─ YES → Implement acknowledgments + idempotencyBinary Data Strategy
Sending binary data with Socket.IO?
├─ Small binary payloads?
│ └─ YES → Send directly in event
│ - Socket.IO handles serialization
│ - ArrayBuffer, Buffer, Blob all supported
├─ Large files?
│ └─ YES → Chunk the uploads
│ - Send metadata first
│ - Stream chunks with progress
│ - Acknowledge each chunk
├─ Frequent small updates (cursor, position)?
│ └─ YES → Use volatile events
│ - socket.volatile.emit()
│ - OK if some are dropped
└─ Mixed binary + JSON?
└─ YES → Supported automatically
- Objects with binary fields work
- Socket.IO parses correctly---
Quick Reference
Socket.IO Client Options
| Option | Type | Default | Description |
|---|---|---|---|
auth | object \ | function | - |
autoConnect | boolean | true | Connect on instantiation |
reconnection | boolean | true | Enable automatic reconnection |
reconnectionAttempts | number | Infinity | Max attempts |
reconnectionDelay | number | 1000 | Initial delay (ms) |
reconnectionDelayMax | number | 5000 | Maximum delay (ms) |
timeout | number | 20000 | Connection timeout (ms) |
transports | string[] | ["polling", "websocket", "webtransport"] | Transport priority |
withCredentials | boolean | false | Send cookies cross-origin |
ackTimeout | number | - | Default acknowledgment timeout (v4.6.0+, requires retries) |
retries | number | - | Max packet retransmission attempts (v4.6.0+) |
tryAllTransports | boolean | false | Test all transports if initial fails (v4.8.0+) |
closeOnBeforeunload | boolean | false | Close silently on browser unload (v4.7.1+) |
Socket Events
| Event | Description |
|---|---|
connect | Connection established |
disconnect | Disconnected (with reason) |
connect_error | Connection error |
Manager Events (socket.io)
| Event | Description |
|---|---|
reconnect_attempt | Attempting to reconnect (with attempt number) |
reconnect | Successfully reconnected |
reconnect_error | Reconnection attempt failed |
reconnect_failed | All reconnection attempts exhausted |
Disconnect Reasons
| Reason | Description | Will Reconnect |
|---|---|---|
io server disconnect | Server called socket.disconnect() | No |
io client disconnect | Client called socket.disconnect() | No |
ping timeout | No pong response from server | Yes |
transport close | Connection closed (network issue) | Yes |
transport error | Connection error | Yes |
Connection Checklist
- [ ] Types defined for ServerToClientEvents and ClientToServerEvents
- [ ] Token in
authoption (NOT query string) - [ ] Named constants for all timing values
- [ ] Connection state tracking with UI feedback
- [ ] Error handling for connect_error
- [ ] Event listener cleanup in useEffect return
- [ ] Check socket.connected before emitting
- [ ] Message queue for offline support
- [ ] Token refresh before reconnection
- [ ] Cleanup on component unmount
Security Checklist
- [ ] Uses HTTPS/WSS in production
- [ ] Token in auth option (not query string)
- [ ] Token refresh mechanism implemented
- [ ] No sensitive data in events without validation
- [ ] CORS properly configured on server
- [ ] Rate limiting on server side
Performance Checklist
- [ ] Single socket instance shared across app
- [ ] Volatile events for expendable data
- [ ] Binary data chunked for large files
- [ ] Event listeners removed when not needed
- [ ] Connection state recovery utilized (v4.6.0+)
- [ ] Namespaces share single connection
Related skills
FAQ
Is Socket.IO the same as WebSocket?
No; Socket.IO is not a WebSocket implementation and adds a protocol layer, so a plain WebSocket client cannot connect to a Socket.IO server.
How should I pass authentication tokens?
Use the auth option for authentication tokens and never pass tokens in query strings.