react-websocket について
ReactでWebSocket機能を実装するためのスキルガイドです。カスタムフック、Contextでの接続共有、Socket.IO、TanStack Queryによる更新を扱います。
主な機能
- 01カスタムWebSocketフック
- 02Contextによる接続共有
- 03Socket.IO連携
- 04TanStack Queryの更新
- 05接続状態の管理
- 06接続のクリーンアップと復旧の指針
活用シーン
- 01リアルタイムチャットルームの構築
- 02Reactコンポーネント間での接続共有
- 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を使用します
参考ドキュメント
コメント
Markdown 対応