mcpField
R
react-websocket

react-websocket

github.com/claude-dev-suite開発4642 回インストール2 日前に更新
リポジトリ

react-websocket について

ReactでWebSocket機能を実装するためのスキルガイドです。カスタムフック、Contextでの接続共有、Socket.IO、TanStack Queryによる更新を扱います。

主な機能

  1. 01カスタムWebSocketフック
  2. 02Contextによる接続共有
  3. 03Socket.IO連携
  4. 04TanStack Queryの更新
  5. 05接続状態の管理
  6. 06接続のクリーンアップと復旧の指針

活用シーン

  1. 01リアルタイムチャットルームの構築
  2. 02Reactコンポーネント間での接続共有
  3. 03ソケットメッセージによるキャッシュ更新

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill react-websocket

インストール後、Claude Code で/react-websocketを入力して読み込まれたか確認します。

詳細

React WebSocket 統合

深い知識:包括的なドキュメントを参照するには、technology: react を指定して mcp__documentation__fetch_docs を使用してください。

カスタム WebSocket フック

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>
  );
}

コンテキストプロバイダーパターン

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>
  );
}

アンチパターン

アンチパターン問題点正しい方法
render 内で WebSocket を作成するrender のたびに新しい接続が作成されますuseRef またはコンテキストを使用します
アンマウント時にクリーンアップしないメモリーリークや接続の重複が発生しますuseEffect でクリーンアップを返します
再接続ロジックがない切断された接続が復旧しません指数バックオフを実装します
ソケットを state に保存する不要な再レンダリングが発生します代わりに useRef を使用します
接続エラーを処理しないエラーが通知されずに発生しますonError ハンドラーを追加します

トラブルシューティングの要点

症状原因対処法
接続がすぐに閉じるCORS または認証の問題サーバーの CORS 設定を確認します
メッセージを受信できないイベントタイプが間違っているメッセージ形式が一致していることを確認します
接続が複数あるクリーンアップがないuseEffect にクリーンアップを追加します
state が更新されない古いクロージャー関数形式の setState を使用します
再接続ループが発生するサーバーに拒否されている最大再接続回数を設定します

本番環境向けチェックリスト

  • 指数バックオフによる再接続
  • 接続ステータスインジケーター
  • アンマウント時の正常な切断
  • エラー処理とログ記録
  • 認証トークンの更新
  • オフライン時のメッセージキュー
  • ハートビート/ping の仕組み

このスキルを使用しない場合

  • サーバー側の WebSocket(NestJS)の場合 → nestjs-websocket を使用します
  • Spring WebSocket の場合 → spring-websocket を使用します
  • シンプルな HTTP ポーリングの場合 → refetchInterval を指定して tanstack-query を使用します

参考ドキュメント

コメント