@metadiv-studio/use-websocket
v0.1.2
Published
A powerful React hook for managing WebSocket connections with automatic reconnection, ping/pong heartbeats, and typed message handling.
Downloads
15
Readme
@metadiv-studio/use-websocket
A powerful React hook for managing WebSocket connections with automatic reconnection, ping/pong heartbeats, and typed message handling.
Features
- 🔄 Automatic Reconnection - Configurable retry logic with exponential backoff
- 💓 Heartbeat/Ping-Pong - Built-in connection health monitoring
- 🔐 Token Authentication - Automatic token injection from localStorage
- 📝 TypeScript Support - Fully typed API with generic message handlers
- 🎯 Action-based Messaging - Structured request/response pattern
- ⚡ React Hooks - Simple integration with React components
- 🛡️ Error Handling - Comprehensive error handling and reporting
Installation
npm install @metadiv-studio/use-websocketQuick Start
import { useWsCall, WsHandlersMap } from '@metadiv-studio/use-websocket';
function MyComponent() {
const handlersMap: WsHandlersMap = {
'message_received': (response) => {
console.log('New message:', response.data);
}
};
const { isConnected, sendMessage } = useWsCall('ws://localhost:8080', {
handlersMap,
onConnect: () => console.log('Connected!'),
onDisconnect: () => console.log('Disconnected!')
});
const handleSend = async () => {
await sendMessage('send_message', { content: 'Hello!' });
};
return (
<div>
<p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
<button onClick={handleSend} disabled={!isConnected}>
Send Message
</button>
</div>
);
}API Reference
useWsCall Hook
const {
isConnected,
isConnecting,
connectionState,
connect,
disconnect,
sendMessage,
sendInitRequest,
ws
} = useWsCall(url, options);Parameters
- url
string- WebSocket server URL - options
UseWsCallOptions- Configuration options
Options
interface UseWsCallOptions {
handlersMap?: WsHandlersMap; // Message handlers by action
autoReconnect?: boolean; // Auto-reconnect on disconnect (default: true)
maxReconnectAttempts?: number; // Max reconnection attempts (default: 5)
pingInterval?: number; // Ping interval in ms (default: 7000)
reconnectDelay?: number; // Base reconnect delay in ms (default: 1000)
onConnect?: () => void; // Connection established callback
onDisconnect?: () => void; // Connection lost callback
onError?: (error: string) => void; // Error callback
}Return Values
- isConnected
boolean- Connection status - isConnecting
boolean- Connection in progress - connectionState
WsConnectionState- Detailed connection state - connect
() => void- Manually connect - disconnect
() => void- Manually disconnect - sendMessage
<T>(action: string, data?: T) => Promise<WsCallResult>- Send message - sendInitRequest
<T>(action: string, data?: T) => Promise<WsCallResult>- Send initial request - ws
WebSocket | null- Raw WebSocket instance
Types
Message Types
interface WsRequest<T = any> {
action: string;
data?: T;
}
interface WsResponse<T = any> {
action: string;
success: boolean;
time: string;
trace_id: string;
data?: T;
error?: string;
page?: {
page: number;
size: number;
total: number;
};
}Handler Types
type WsMessageHandler<T = any> = (message: WsResponse<T>) => void;
type WsHandlersMap = Record<string, WsMessageHandler>;Connection State
interface WsConnectionState {
readyState: number;
isConnected: boolean;
isConnecting: boolean;
lastError?: string;
reconnectAttempts: number;
}Authentication
The hook automatically retrieves authentication tokens from localStorage using the key "token". Make sure to set your token before connecting:
// Set token before using the hook
localStorage.setItem('token', 'your_jwt_token_here');The token is automatically appended to the WebSocket URL as a query parameter.
Complete Usage Example
import React, { useCallback, useState } from 'react';
import { useWsCall, WsHandlersMap, WsResponse } from '@metadiv-studio/use-websocket';
// Example usage of useWsCall hook
export const WebSocketExample: React.FC = () => {
const [messages, setMessages] = useState<string[]>([]);
const [inputMessage, setInputMessage] = useState('');
// Define handlers for different WebSocket actions
const handlersMap: WsHandlersMap = {
// Chat message handler
'chat_message': useCallback((message: WsResponse<{ user: string; content: string; timestamp: string }>) => {
if (message.success && message.data) {
const chatMsg = `[${message.data.timestamp}] ${message.data.user}: ${message.data.content}`;
setMessages(prev => [...prev, chatMsg]);
}
}, []),
// User joined handler
'user_joined': useCallback((message: WsResponse<{ user: string; timestamp: string }>) => {
if (message.success && message.data) {
const joinMsg = `[${message.data.timestamp}] ${message.data.user} joined the chat`;
setMessages(prev => [...prev, joinMsg]);
}
}, []),
// User left handler
'user_left': useCallback((message: WsResponse<{ user: string; timestamp: string }>) => {
if (message.success && message.data) {
const leaveMsg = `[${message.data.timestamp}] ${message.data.user} left the chat`;
setMessages(prev => [...prev, leaveMsg]);
}
}, []),
// Error handler
'error': useCallback((message: WsResponse) => {
if (!message.success && message.error) {
setMessages(prev => [...prev, `ERROR: ${message.error}`]);
}
}, []),
// System notification handler
'system_notification': useCallback((message: WsResponse<{ message: string; type: 'info' | 'warning' | 'error' }>) => {
if (message.success && message.data) {
const systemMsg = `[SYSTEM ${message.data.type.toUpperCase()}] ${message.data.message}`;
setMessages(prev => [...prev, systemMsg]);
}
}, [])
};
// Initialize WebSocket connection
const {
isConnected,
isConnecting,
connectionState,
connect,
disconnect,
sendMessage,
sendInitRequest
} = useWsCall('ws://localhost:8080/v1/chat', {
handlersMap,
autoReconnect: true,
maxReconnectAttempts: 5,
pingInterval: 7000, // 7 seconds
onConnect: () => {
console.log('Connected to chat server');
setMessages(prev => [...prev, '[SYSTEM] Connected to server']);
// Send initial request to join chat room
sendInitRequest('join_room', {
room: 'general',
user: 'current_user'
}).catch(console.error);
},
onDisconnect: () => {
console.log('Disconnected from chat server');
setMessages(prev => [...prev, '[SYSTEM] Disconnected from server']);
},
onError: (error) => {
console.error('WebSocket error:', error);
setMessages(prev => [...prev, `[ERROR] ${error}`]);
}
});
// Send chat message
const handleSendMessage = useCallback(async () => {
if (!inputMessage.trim() || !isConnected) return;
try {
await sendMessage('send_chat_message', {
content: inputMessage.trim(),
room: 'general'
});
setInputMessage('');
} catch (error) {
console.error('Failed to send message:', error);
setMessages(prev => [...prev, `[ERROR] Failed to send message: ${error}`]);
}
}, [inputMessage, isConnected, sendMessage]);
// Send typing indicator
const handleTyping = useCallback(async () => {
if (isConnected) {
try {
await sendMessage('typing', { room: 'general' });
} catch (error) {
console.error('Failed to send typing indicator:', error);
}
}
}, [isConnected, sendMessage]);
// Request user list
const handleRequestUserList = useCallback(async () => {
if (isConnected) {
try {
await sendMessage('get_user_list', { room: 'general' });
} catch (error) {
console.error('Failed to request user list:', error);
}
}
}, [isConnected, sendMessage]);
return (
<div className="max-w-2xl mx-auto p-4">
<div className="bg-white rounded-lg shadow-lg">
{/* Header */}
<div className="p-4 border-b">
<h2 className="text-xl font-semibold">WebSocket Chat Example</h2>
<div className="flex items-center space-x-4 mt-2">
<div className={`flex items-center space-x-2 ${
isConnected ? 'text-green-600' : isConnecting ? 'text-yellow-600' : 'text-red-600'
}`}>
<div className={`w-2 h-2 rounded-full ${
isConnected ? 'bg-green-600' : isConnecting ? 'bg-yellow-600' : 'bg-red-600'
}`}></div>
<span className="text-sm">
{isConnected ? 'Connected' : isConnecting ? 'Connecting...' : 'Disconnected'}
</span>
</div>
{connectionState.reconnectAttempts > 0 && (
<span className="text-sm text-gray-500">
Reconnect attempts: {connectionState.reconnectAttempts}
</span>
)}
</div>
{/* Connection Controls */}
<div className="flex space-x-2 mt-3">
<button
onClick={connect}
disabled={isConnected || isConnecting}
className="px-3 py-1 bg-blue-500 text-white rounded text-sm disabled:bg-gray-400"
>
Connect
</button>
<button
onClick={disconnect}
disabled={!isConnected}
className="px-3 py-1 bg-red-500 text-white rounded text-sm disabled:bg-gray-400"
>
Disconnect
</button>
<button
onClick={handleRequestUserList}
disabled={!isConnected}
className="px-3 py-1 bg-green-500 text-white rounded text-sm disabled:bg-gray-400"
>
Get Users
</button>
</div>
</div>
{/* Messages */}
<div className="h-96 overflow-y-auto p-4 bg-gray-50">
{messages.length === 0 ? (
<div className="text-gray-500 text-center">No messages yet...</div>
) : (
<div className="space-y-2">
{messages.map((message, index) => (
<div
key={index}
className="text-sm font-mono p-2 bg-white rounded border"
>
{message}
</div>
))}
</div>
)}
</div>
{/* Message Input */}
<div className="p-4 border-t">
<div className="flex space-x-2">
<input
type="text"
value={inputMessage}
onChange={(e) => {
setInputMessage(e.target.value);
handleTyping(); // Send typing indicator
}}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSendMessage();
}
}}
placeholder="Type a message..."
className="flex-1 px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={!isConnected}
/>
<button
onClick={handleSendMessage}
disabled={!isConnected || !inputMessage.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-400"
>
Send
</button>
</div>
</div>
</div>
</div>
);
};Message Protocol
The hook expects WebSocket messages to follow this protocol:
Request Format
{
"action": "action_name",
"data": { /* optional payload */ }
}Response Format
{
"action": "action_name",
"success": true,
"time": "2023-12-01T10:00:00Z",
"trace_id": "unique-trace-id",
"data": { /* response payload */ },
"error": "error message if success is false",
"page": { /* optional pagination info */ }
}Built-in Actions
- ping/pong: Automatic heartbeat mechanism
- Custom actions are defined by your application
Error Handling
The hook provides comprehensive error handling:
const { isConnected, sendMessage } = useWsCall(url, {
onError: (error) => {
console.error('WebSocket error:', error);
// Handle error (show notification, etc.)
}
});
// Handle send errors
try {
await sendMessage('action', data);
} catch (error) {
console.error('Failed to send message:', error);
}Best Practices
- Use useCallback for handlers to prevent unnecessary re-renders
- Set authentication token in localStorage before connecting
- Handle connection states in your UI (connecting, connected, disconnected)
- Implement proper error handling for both connection and message errors
- Use TypeScript for better type safety with message payloads
- Clean up properly - the hook handles cleanup automatically on unmount
Peer Dependencies
- React 18+
- React DOM 18+
Make sure these are installed in your project:
npm install react@^18 react-dom@^18