@loanpal/react-websocket
v0.1.2
Published
React webSocket client
Maintainers
Keywords
Readme
About
This is a hook-based React websocket implementation.
Installation
From your react project folder, run:
pnpm add @loanpal/react-websocket
WebSocket Implementation
This directory contains the WebSocket implementation using Zustand for state management.
Components
websocket-client.ts
The core WebSocket client class that handles:
- Connection management
- Automatic reconnection (max 3 retries)
- Message handling and delivery to all listeners
- Subscription management
Key Features:
- Only one connection is created per URL
- Automatic reconnection on connection loss
- Type-safe message handling
- Simple single-channel design
websocket-store.ts
Zustand store that manages:
- Multiple WebSocket clients by URL
- Client lifecycle (creation, cleanup)
- Global state access
use-websocket.tsx
React hook for subscribing to WebSocket messages:
useWebsocket({
componentName: "LeadDetailsPage",
url: "ws://localhost:8000/api/v1/leads/019a0c08-3364-7afe-9bec-9ce9c5709b4e/ws",
connectionId: "lead-updates", // Unique ID for this connection
protocols: `access_token.${getAccessToken()}`, // Optional: Pass auth via subprotocol
messageFilter: (msg) => msg.subject === "lead-123", // Optional: Filter messages
auth: {
// Option 1: Plain object sent as first message after connection
forwardedProps: {
app_id: "HOMEOWNER",
access_token: getAccessToken(),
},
// ... other required fields
},
// Or Option 2: Function for custom auth logic
// auth: (send) => {
// send({
// forwardedProps: {
// app_id: "HOMEOWNER",
// access_token: getAccessToken(),
// },
// });
// },
onMessage: (message) => {
console.log("Received:", message);
},
});Features:
- Multiple connections per URL: Use different
connectionIdvalues to create separate physical connections to the same URL - Message filtering: Optional
messageFiltercallback to selectively receive messages - Internally memoizes callbacks (no need for
useCallback) - Auto-cleanup on unmount
- Optional
protocolsparameter for passing auth tokens or headers via WebSocket subprotocol - Optional
authparameter for sending authentication on connection (supports both plain object and function)
websocket-debug-panel.tsx

Development-only UI component that displays:
- Active WebSocket connections
- Connection status (CONNECTING, OPEN, CLOSING, CLOSED)
- Number of listeners per connection
- Real-time updates every second
Usage:
import { WebsocketDebugPanel } from "@loanpal/react-websocket";
// inside your applicaiton code, simply render the component
return (
<WebsocketDebugPanel />
)Usage Example
import { useWebsocket } from "@loanpal/react-websocket";
import { getAccessToken } from "@/shared/utils/cookie-handler";
function MyComponent() {
const { send, isConnected } = useWebsocket({
componentName: "MyComponent",
url: "ws://localhost:8000/api/v1/ws",
connectionId: "my-connection", // Required: unique ID for this connection
// Option 1: Pass auth via subprotocol header
protocols: `access_token.${getAccessToken()}`,
// Option 2: Send auth as plain object (first message after connection)
auth: {
forwardedProps: {
app_id: "HOMEOWNER",
access_token: getAccessToken(),
},
// ... other required fields from AGUIUserFirstMessageType
},
// Option 3: Or use function for custom auth logic
// auth: (send) => {
// send({
// forwardedProps: {
// app_id: "HOMEOWNER",
// access_token: getAccessToken(),
// },
// });
// },
onMessage: (message) => {
// Handle all messages
console.log("Received message:", message);
},
});
// Send messages using the send function
const sendCustomMessage = () => {
send({ type: "custom", data: "hello" });
};
return (
<div>
My Component - {isConnected ? "Connected" : "Disconnected"}
</div>
);
}Message Filtering
The messageFilter parameter allows you to selectively receive messages based on custom criteria. This is particularly useful when the server sends messages with identifying fields (like subject, channel, or topic) and you only want to handle specific messages:
type SubjectMessage = {
subject: string;
data: any;
};
function UserProfile({ userId }: { userId: string }) {
// Only receive messages for this specific user
const { send, isConnected } = useWebsocket<SubjectMessage>({
url: "ws://api/updates",
connectionId: "user-updates",
messageFilter: (msg) => msg.subject === userId,
onMessage: (message) => {
// This will only be called for messages matching the userId
console.log("Update for user:", message.data);
},
});
const sendUpdate = () => {
send({ subject: userId, data: { status: "active" } });
};
return <div>User Profile - {isConnected ? "Online" : "Offline"}</div>;
}Key Points:
- The filter is applied before
onMessageis called - Filtered-out messages are silently ignored (onMessage won't be called)
- The filter function receives the fully typed message
- If
messageFilteris not provided, all messages are passed toonMessage
Common Use Cases:
// Filter by subject/channel
messageFilter: (msg) => msg.subject === "user-123"
// Filter by message type
messageFilter: (msg) => msg.type === "notification"
// Filter by multiple criteria
messageFilter: (msg) =>
msg.channel === "updates" && msg.priority === "high"
// Complex filtering logic
messageFilter: (msg) => {
if (msg.type === "alert") return true;
if (msg.type === "update" && msg.userId === currentUserId) return true;
return false;
}Connection Identity & Multiple Connections
The websocket implementation uses a composite key of url:connectionId to identify unique connections:
// These create TWO separate physical WebSocket connections
const { send: send1 } = useWebsocket({
url: "ws://example.com/api",
connectionId: "session-1", // Connection 1
onMessage: (msg) => console.log("Session 1:", msg)
});
const { send: send2 } = useWebsocket({
url: "ws://example.com/api",
connectionId: "session-2", // Connection 2 (different physical connection)
onMessage: (msg) => console.log("Session 2:", msg)
});
// These share the SAME physical WebSocket connection
const { send: send3 } = useWebsocket({
url: "ws://example.com/api",
connectionId: "session-1", // Reuses Connection 1
onMessage: (msg) => console.log("Also Session 1:", msg)
});Key behaviors:
- Same
url+ sameconnectionId= shared connection (all listeners receive all messages) - Same
url+ differentconnectionId= separate connections (each receives different server data) - Requires backend support for multiple concurrent connections per user
Generic Type Support
The useWebsocket hook requires you to specify your message type for full type safety:
// Define your message types
type ChatMessage = {
type: 'chat';
user: string;
text: string;
timestamp: number;
};
type NotificationMessage = {
type: 'notification';
title: string;
body: string;
priority: 'high' | 'low';
};
// Use with specific message type (type parameter is required)
const { send: sendChat, isConnected: chatConnected } = useWebsocket<ChatMessage>({
url: "ws://api/chat",
connectionId: "chat-room-1",
onMessage: (msg) => {
// msg is fully typed as ChatMessage!
console.log(`${msg.user}: ${msg.text}`);
}
});
// Send a chat message
sendChat({ type: 'chat', user: 'Alice', text: 'Hello!', timestamp: Date.now() });
// Different connection with different message type
const { send: sendNotif, isConnected: notifConnected } = useWebsocket<NotificationMessage>({
url: "ws://api/notifications",
connectionId: "user-notifications",
onMessage: (msg) => {
// msg is fully typed as NotificationMessage!
if (msg.priority === 'high') {
alert(msg.title);
}
}
});Creating Typed Wrapper Hooks
For cleaner APIs, create client-specific wrapper hooks:
// hooks/use-chat-websocket.ts
export const useChatWebsocket = (roomId: string, onMessage: (msg: ChatMessage) => void) => {
return useWebsocket<ChatMessage>({
url: `ws://api/chat/${roomId}`,
connectionId: `chat-${roomId}`,
onMessage,
componentName: 'ChatClient',
});
};
// Usage - much cleaner!
const { send, isConnected } = useChatWebsocket('room-123', (msg) => {
console.log(msg.user, msg.text); // Fully typed!
});
// Send a message
if (isConnected) {
send({ type: 'chat', user: 'Bob', text: 'Hi!', timestamp: Date.now() });
}Architecture
┌─────────────────────┐
│ useWebsocket │
│ (Hook) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ WebsocketStore │
│ (Zustand) │
│ Key: url:connId │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ WebsocketClient │
│ (Class) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Native WebSocket |
│ (Class) │
└─────────────────────┘Key Benefits
- Multiple Connections per URL: Create separate connections using different
connectionIdvalues - Shared Connections: Components with the same
url:connectionIdshare a single connection - Generic Type Support: Fully typed messages using TypeScript generics for different message schemas
- No Provider Needed: No need to wrap components in a provider
- Auto-memoization: Callbacks are internally memoized
- Type-safe: Full TypeScript support with compile-time type checking
- Debug UI: Easy debugging with the debug panel
