mcpField
R
react-websocket

react-websocket

github.com/claude-dev-suite开发4642 次安装2 天前更新
代码仓库

关于 react-websocket

面向 React 开发者的 WebSocket 技能指南,涵盖自定义 Hook、Context 共享连接、Socket.IO 和 TanStack Query 实时更新。

主要功能

  1. 01自定义 WebSocket Hook
  2. 02通过 Context 共享连接
  3. 03Socket.IO 集成
  4. 04TanStack Query 实时更新
  5. 05连接状态处理
  6. 06连接清理与恢复建议

适用场景

  1. 01构建实时聊天房间
  2. 02在 React 组件间共享连接
  3. 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