关于 react-websocket
面向 React 开发者的 WebSocket 技能指南,涵盖自定义 Hook、Context 共享连接、Socket.IO 和 TanStack Query 实时更新。
主要功能
- 01自定义 WebSocket Hook
- 02通过 Context 共享连接
- 03Socket.IO 集成
- 04TanStack Query 实时更新
- 05连接状态处理
- 06连接清理与恢复建议
适用场景
- 01构建实时聊天房间
- 02在 React 组件间共享连接
- 03根据 socket 消息更新缓存数据
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill react-websocket安装后在 Claude Code 中输入/react-websocket确认已加载。
详细介绍
React WebSocket 集成
深入了解:使用
mcp__documentation__fetch_docs,技术选项为:react,获取全面的文档。
自定义 WebSocket Hook
import { useState, useEffect, useCallback, useRef } from 'react';
interface UseWebSocketOptions {
url: string;
onMessage?: (data: unknown) => void;
onOpen?: () => void;
onClose?: () => void;
onError?: (error: Event) => void;
reconnect?: boolean;
reconnectInterval?: number;
reconnectAttempts?: number;
}
export function useWebSocket({
url,
onMessage,
onOpen,
onClose,
onError,
reconnect = true,
reconnectInterval = 3000,
reconnectAttempts = 5,
}: UseWebSocketOptions) {
const [isConnected, setIsConnected] = useState(false);
const [lastMessage, setLastMessage] = useState<unknown>(null);
const wsRef = useRef<WebSocket | null>(null);
const reconnectCountRef = useRef(0);
const connect = useCallback(() => {
const ws = new WebSocket(url);
ws.onopen = () => {
setIsConnected(true);
reconnectCountRef.current = 0;
onOpen?.();
};
ws.onclose = () => {
setIsConnected(false);
onClose?.();
if (reconnect && reconnectCountRef.current < reconnectAttempts) {
reconnectCountRef.current++;
setTimeout(connect, reconnectInterval);
}
};
ws.onerror = (error) => {
onError?.(error);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
setLastMessage(data);
onMessage?.(data);
};
wsRef.current = ws;
}, [url, onMessage, onOpen, onClose, onError, reconnect, reconnectInterval, reconnectAttempts]);
useEffect(() => {
connect();
return () => {
wsRef.current?.close();
};
}, [connect]);
const send = useCallback((data: unknown) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(data));
}
}, []);
const disconnect = useCallback(() => {
wsRef.current?.close();
}, []);
return { isConnected, lastMessage, send, disconnect };
}
使用示例
function ChatRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<Message[]>([]);
const { isConnected, send } = useWebSocket({
url: `wss://api.example.com/ws?room=${roomId}`,
onMessage: (data) => {
setMessages((prev) => [...prev, data as Message]);
},
onOpen: () => {
send({ type: 'join', room: roomId });
},
});
const handleSend = (text: string) => {
send({ type: 'message', content: text, room: roomId });
};
return (
<div>
<div>Status: {isConnected ? 'Connected' : 'Disconnected'}</div>
<MessageList messages={messages} />
<MessageInput onSend={handleSend} disabled={!isConnected} />
</div>
);
}
Context Provider 模式
import { createContext, useContext, ReactNode } from 'react';
interface WebSocketContextValue {
isConnected: boolean;
send: (data: unknown) => void;
subscribe: (event: string, handler: (data: unknown) => void) => () => void;
}
const WebSocketContext = createContext<WebSocketContextValue | null>(null);
export function WebSocketProvider({ url, children }: { url: string; children: ReactNode }) {
const [isConnected, setIsConnected] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
const handlersRef = useRef<Map<string, Set<(data: unknown) => void>>>(new Map());
useEffect(() => {
const ws = new WebSocket(url);
ws.onopen = () => setIsConnected(true);
ws.onclose = () => setIsConnected(false);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
const handlers = handlersRef.current.get(data.type);
handlers?.forEach((handler) => handler(data));
};
wsRef.current = ws;
return () => ws.close();
}, [url]);
const send = useCallback((data: unknown) => {
wsRef.current?.send(JSON.stringify(data));
}, []);
const subscribe = useCallback((event: string, handler: (data: unknown) => void) => {
if (!handlersRef.current.has(event)) {
handlersRef.current.set(event, new Set());
}
handlersRef.current.get(event)!.add(handler);
return () => {
handlersRef.current.get(event)?.delete(handler);
};
}, []);
return (
<WebSocketContext.Provider value={{ isConnected, send, subscribe }}>
{children}
</WebSocketContext.Provider>
);
}
export function useWS() {
const context = useContext(WebSocketContext);
if (!context) throw new Error('useWS must be used within WebSocketProvider');
return context;
}
Socket.IO 集成
import { io, Socket } from 'socket.io-client';
import { useEffect, useRef, useState, useCallback } from 'react';
export function useSocketIO(url: string) {
const [isConnected, setIsConnected] = useState(false);
const socketRef = useRef<Socket | null>(null);
useEffect(() => {
const socket = io(url, {
auth: { token: getAuthToken() },
reconnection: true,
reconnectionAttempts: 5,
});
socket.on('connect', () => setIsConnected(true));
socket.on('disconnect', () => setIsConnected(false));
socketRef.current = socket;
return () => {
socket.disconnect();
};
}, [url]);
const emit = useCallback((event: string, data: unknown) => {
socketRef.current?.emit(event, data);
}, []);
const on = useCallback((event: string, handler: (data: unknown) => void) => {
socketRef.current?.on(event, handler);
return () => {
socketRef.current?.off(event, handler);
};
}, []);
return { isConnected, emit, on, socket: socketRef.current };
}
TanStack Query 集成
import { useQueryClient } from '@tanstack/react-query';
function useRealtimeUpdates() {
const queryClient = useQueryClient();
useWebSocket({
url: 'wss://api.example.com/ws',
onMessage: (data: any) => {
switch (data.type) {
case 'user_updated':
queryClient.invalidateQueries({ queryKey: ['users', data.userId] });
break;
case 'message_created':
queryClient.setQueryData(['messages', data.roomId], (old: Message[]) => [
...old,
data.message,
]);
break;
}
},
});
}
连接状态组件
function ConnectionStatus() {
const { isConnected } = useWS();
return (
<div
className={`flex items-center gap-2 ${isConnected ? 'text-green-500' : 'text-red-500'}`}
>
<span className={`h-2 w-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
{isConnected ? 'Connected' : 'Disconnected'}
</div>
);
}
反模式
| 反模式 | 问题所在 | 正确做法 |
|---|---|---|
| 在渲染过程中创建 WebSocket | 每次渲染都会创建新连接 | 使用 useRef 或 context |
| 卸载时未清理 | 内存泄漏、重复连接 | 在 useEffect 中返回清理函数 |
| 缺少重连逻辑 | 无法恢复已断开的连接 | 实现指数退避 |
| 将 socket 存储在 state 中 | 造成不必要的重新渲染 | 改用 useRef |
| 未处理连接错误 | 错误不会显现 | 添加 onError 处理程序 |
快速故障排查
| 症状 | 原因 | 解决方法 |
|---|---|---|
| 连接立即关闭 | CORS 或身份验证问题 | 检查服务端的 CORS 配置 |
| 未收到消息 | 事件类型错误 | 确认消息格式匹配 |
| 存在多个连接 | 缺少清理逻辑 | 在 useEffect 中添加清理逻辑 |
| 状态未更新 | 闭包中的值已过时 | 使用函数式 setState |
| 不断重连 | 服务端拒绝连接 | 添加最大重连次数 |
生产环境检查清单
- 使用指数退避重连
- 连接状态指示器
- 卸载时正常断开连接
- 错误处理和日志记录
- 身份验证令牌刷新
- 离线时使用消息队列
- 心跳/ping 机制
不适用此技能的场景
- 对于服务端 WebSocket(NestJS)→ 使用
nestjs-websocket - 对于 Spring WebSocket → 使用
spring-websocket - 对于简单的 HTTP 轮询 → 使用带有 refetchInterval 的
tanstack-query
参考文档
评论
支持 Markdown