HTTP ist ein Request-Response-Protokoll: Client fragt, Server antwortet, Verbindung schließt. Für Echtzeitdaten ist das ineffizient. Polling (alle 5 Sekunden neu fragen) verschwendet Bandbreite und hat Latency. Long-Polling ist ein Hack. Server-Sent Events sind unidirektional.
WebSockets lösen das Problem elegant: Eine persistente, bidirektionale Verbindung. Einmal aufgebaut, können beide Seiten jederzeit Nachrichten senden – kein Polling, minimale Latency, perfekt für Live-Chats, Real-time Dashboards, Collaborative Editing, Gaming.
Traditionelles Polling:
// ❌ Ineffizient: Alle 3 Sekunden HTTP-Request
function LiveDashboard() {
const [data, setData] = useState<MetricData | null>(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetch('/api/metrics');
const json = await response.json();
setData(json);
};
fetchData();
const interval = setInterval(fetchData, 3000);
return () => clearInterval(interval);
}, []);
return <div>{/* Render data */}</div>;
}
Probleme: - 3 Sekunden Latency minimum - Jeder Request hat HTTP-Overhead (Headers, Handshake) - Server bearbeitet Requests auch wenn keine neuen Daten - Skaliert schlecht (1000 Clients = 333 Requests/Sekunde)
WebSocket-Lösung:
// ✓ Effizient: Persistente Verbindung, Daten kommen sofort
function LiveDashboard() {
const [data, setData] = useState<MetricData | null>(null);
useEffect(() => {
const ws = new WebSocket('wss://api.example.com/metrics');
ws.onmessage = (event) => {
const json = JSON.parse(event.data);
setData(json); // Sofortiges Update bei neuen Daten
};
return () => ws.close();
}, []);
return <div>{/* Render data */}</div>;
}
Vorteile: - ~1-10ms Latency (nicht 3000ms) - Kein HTTP-Overhead nach Initial-Handshake - Server sendet nur bei Änderungen - Skaliert besser (1000 Clients = eine Verbindung pro Client)
import { useEffect, useState } from 'react';
interface Message {
id: string;
text: string;
timestamp: number;
}
function ChatRoom() {
const [messages, setMessages] = useState<Message[]>([]);
const [connected, setConnected] = useState(false);
useEffect(() => {
const ws = new WebSocket('wss://chat.example.com');
// Verbindung hergestellt
ws.onopen = () => {
console.log('WebSocket connected');
setConnected(true);
};
// Nachricht empfangen
ws.onmessage = (event: MessageEvent) => {
const message: Message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
// Verbindung geschlossen
ws.onclose = () => {
console.log('WebSocket disconnected');
setConnected(false);
};
// Fehler
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Cleanup bei Unmount
return () => {
ws.close();
};
}, []);
const sendMessage = (text: string) => {
// WebSocket-Referenz aus useEffect ist nicht verfügbar hier!
// Problem: Wie sende ich Messages?
};
return (
<div>
<div>Status: {connected ? 'Connected' : 'Disconnected'}</div>
{messages.map(msg => (
<div key={msg.id}>{msg.text}</div>
))}
</div>
);
}
Problem: WebSocket-Referenz ist nur in
useEffect verfügbar. Lösung: useRef.
// hooks/useWebSocket.ts
import { useEffect, useRef, useState, useCallback } from 'react';
export enum WebSocketStatus {
CONNECTING = 'CONNECTING',
CONNECTED = 'CONNECTED',
DISCONNECTED = 'DISCONNECTED',
ERROR = 'ERROR'
}
interface UseWebSocketOptions {
url: string;
onMessage?: (data: any) => 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 ws = useRef<WebSocket | null>(null);
const reconnectCount = useRef(0);
const reconnectTimeout = useRef<NodeJS.Timeout>();
const [status, setStatus] = useState<WebSocketStatus>(WebSocketStatus.CONNECTING);
const [lastMessage, setLastMessage] = useState<any>(null);
const connect = useCallback(() => {
try {
ws.current = new WebSocket(url);
ws.current.onopen = () => {
setStatus(WebSocketStatus.CONNECTED);
reconnectCount.current = 0;
onOpen?.();
};
ws.current.onmessage = (event: MessageEvent) => {
try {
const data = JSON.parse(event.data);
setLastMessage(data);
onMessage?.(data);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
ws.current.onclose = () => {
setStatus(WebSocketStatus.DISCONNECTED);
onClose?.();
// Auto-Reconnect
if (reconnect && reconnectCount.current < reconnectAttempts) {
reconnectCount.current++;
console.log(`Reconnecting... (${reconnectCount.current}/${reconnectAttempts})`);
reconnectTimeout.current = setTimeout(() => {
connect();
}, reconnectInterval);
}
};
ws.current.onerror = (error) => {
setStatus(WebSocketStatus.ERROR);
onError?.(error);
};
} catch (error) {
console.error('Failed to create WebSocket:', error);
setStatus(WebSocketStatus.ERROR);
}
}, [url, onMessage, onOpen, onClose, onError, reconnect, reconnectInterval, reconnectAttempts]);
useEffect(() => {
connect();
return () => {
if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current);
}
ws.current?.close();
};
}, [connect]);
const send = useCallback((data: any) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(data));
} else {
console.warn('WebSocket is not connected');
}
}, []);
const disconnect = useCallback(() => {
reconnectCount.current = reconnectAttempts; // Prevent auto-reconnect
ws.current?.close();
}, [reconnectAttempts]);
return {
status,
lastMessage,
send,
disconnect,
connect
};
}// ChatRoom.tsx
import { useState, useRef, useEffect } from 'react';
import { useWebSocket, WebSocketStatus } from './hooks/useWebSocket';
interface ChatMessage {
id: string;
userId: string;
username: string;
text: string;
timestamp: number;
}
function ChatRoom({ roomId, userId, username }: ChatRoomProps) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
const { status, send } = useWebSocket({
url: `wss://chat.example.com/rooms/${roomId}`,
onMessage: (data) => {
if (data.type === 'message') {
setMessages(prev => [...prev, data.message]);
} else if (data.type === 'history') {
setMessages(data.messages);
}
},
onOpen: () => {
// Bei Verbindung: Join Room
send({ type: 'join', roomId, userId, username });
},
reconnect: true,
reconnectAttempts: 10
});
// Auto-scroll zu neuen Nachrichten
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleSend = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
send({
type: 'message',
text: input,
userId,
username
});
setInput('');
};
const statusColor = {
[WebSocketStatus.CONNECTING]: 'yellow',
[WebSocketStatus.CONNECTED]: 'green',
[WebSocketStatus.DISCONNECTED]: 'red',
[WebSocketStatus.ERROR]: 'red'
};
return (
<div className="chat-room">
<header>
<h2>Room: {roomId}</h2>
<div
className="status-indicator"
style={{ backgroundColor: statusColor[status] }}
/>
<span>{status}</span>
</header>
<div className="messages">
{messages.map(msg => (
<div
key={msg.id}
className={msg.userId === userId ? 'message-own' : 'message-other'}
>
<strong>{msg.username}</strong>
<p>{msg.text}</p>
<time>{new Date(msg.timestamp).toLocaleTimeString()}</time>
</div>
))}
<div ref={messagesEndRef} />
</div>
<form onSubmit={handleSend}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={status !== WebSocketStatus.CONNECTED}
/>
<button
type="submit"
disabled={status !== WebSocketStatus.CONNECTED || !input.trim()}
>
Send
</button>
</form>
</div>
);
}
Problem: JSON.parse(event.data) ist any.
Lösung: Message-Types definieren.
// types/websocket-messages.ts
export enum MessageType {
JOIN = 'join',
LEAVE = 'leave',
MESSAGE = 'message',
TYPING = 'typing',
HISTORY = 'history',
USER_LIST = 'user_list',
ERROR = 'error'
}
// Base Message
interface BaseMessage {
type: MessageType;
timestamp: number;
}
// Client → Server Messages
export interface JoinMessage extends BaseMessage {
type: MessageType.JOIN;
roomId: string;
userId: string;
username: string;
}
export interface ChatMessageOut extends BaseMessage {
type: MessageType.MESSAGE;
text: string;
userId: string;
username: string;
}
export interface TypingMessage extends BaseMessage {
type: MessageType.TYPING;
userId: string;
username: string;
isTyping: boolean;
}
export type ClientMessage = JoinMessage | ChatMessageOut | TypingMessage;
// Server → Client Messages
export interface ChatMessageIn extends BaseMessage {
type: MessageType.MESSAGE;
message: {
id: string;
userId: string;
username: string;
text: string;
timestamp: number;
};
}
export interface HistoryMessage extends BaseMessage {
type: MessageType.HISTORY;
messages: Array<{
id: string;
userId: string;
username: string;
text: string;
timestamp: number;
}>;
}
export interface UserListMessage extends BaseMessage {
type: MessageType.USER_LIST;
users: Array<{
userId: string;
username: string;
online: boolean;
}>;
}
export interface ErrorMessage extends BaseMessage {
type: MessageType.ERROR;
error: string;
}
export type ServerMessage =
| ChatMessageIn
| HistoryMessage
| UserListMessage
| ErrorMessage;// useWebSocket mit Types
import type { ServerMessage, ClientMessage } from '../types/websocket-messages';
interface UseWebSocketOptions {
url: string;
onMessage?: (message: ServerMessage) => void;
// ...
}
export function useWebSocket({ url, onMessage }: UseWebSocketOptions) {
// ...
ws.current.onmessage = (event: MessageEvent) => {
const message: ServerMessage = JSON.parse(event.data);
// TypeScript kennt jetzt alle möglichen Message-Typen
switch (message.type) {
case MessageType.MESSAGE:
// TypeScript weiß: message.message existiert
setMessages(prev => [...prev, message.message]);
break;
case MessageType.HISTORY:
// TypeScript weiß: message.messages existiert
setMessages(message.messages);
break;
case MessageType.USER_LIST:
// TypeScript weiß: message.users existiert
setUsers(message.users);
break;
case MessageType.ERROR:
console.error(message.error);
break;
}
onMessage?.(message);
};
const send = useCallback((message: ClientMessage) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(message));
}
}, []);
return { send, /* ... */ };
}
Einfaches Retry alle 3 Sekunden ist zu aggressiv. Besser: Exponential Backoff.
// hooks/useWebSocket.ts - Erweitert mit Exponential Backoff
export function useWebSocket(options: UseWebSocketOptions) {
const reconnectDelay = useRef(1000); // Start bei 1s
const maxReconnectDelay = 30000; // Max 30s
const connect = useCallback(() => {
// ... WebSocket setup
ws.current.onclose = () => {
setStatus(WebSocketStatus.DISCONNECTED);
if (reconnect && reconnectCount.current < reconnectAttempts) {
reconnectCount.current++;
const delay = Math.min(
reconnectDelay.current * Math.pow(2, reconnectCount.current - 1),
maxReconnectDelay
);
console.log(`Reconnecting in ${delay}ms... (${reconnectCount.current}/${reconnectAttempts})`);
reconnectTimeout.current = setTimeout(() => {
connect();
}, delay);
}
};
}, [/* ... */]);
// Reset Delay bei erfolgreicher Verbindung
ws.current.onopen = () => {
setStatus(WebSocketStatus.CONNECTED);
reconnectCount.current = 0;
reconnectDelay.current = 1000; // Reset auf 1s
onOpen?.();
};
}Backoff-Sequenz: - 1. Versuch: 1s - 2. Versuch: 2s - 3. Versuch: 4s - 4. Versuch: 8s - 5. Versuch: 16s - 6. Versuch: 30s (capped)
Manche Networks/Proxies schließen idle Connections. Lösung: Ping/Pong.
export function useWebSocket(options: UseWebSocketOptions) {
const heartbeatInterval = useRef<NodeJS.Timeout>();
const heartbeatTimeout = useRef<NodeJS.Timeout>();
const startHeartbeat = useCallback(() => {
heartbeatInterval.current = setInterval(() => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify({ type: 'ping' }));
// Erwarte Pong innerhalb 5s
heartbeatTimeout.current = setTimeout(() => {
console.warn('No pong received, connection may be dead');
ws.current?.close();
}, 5000);
}
}, 30000); // Ping alle 30s
}, []);
const stopHeartbeat = useCallback(() => {
if (heartbeatInterval.current) {
clearInterval(heartbeatInterval.current);
}
if (heartbeatTimeout.current) {
clearTimeout(heartbeatTimeout.current);
}
}, []);
ws.current.onmessage = (event: MessageEvent) => {
const message = JSON.parse(event.data);
// Pong empfangen
if (message.type === 'pong') {
if (heartbeatTimeout.current) {
clearTimeout(heartbeatTimeout.current);
}
return;
}
// Normale Message-Verarbeitung
onMessage?.(message);
};
ws.current.onopen = () => {
setStatus(WebSocketStatus.CONNECTED);
startHeartbeat();
onOpen?.();
};
ws.current.onclose = () => {
stopHeartbeat();
// ... reconnect logic
};
return () => {
stopHeartbeat();
ws.current?.close();
};
}// LiveMetrics.tsx
import { useWebSocket, WebSocketStatus } from './hooks/useWebSocket';
import { useState } from 'react';
interface Metrics {
cpu: number;
memory: number;
requests: number;
errors: number;
timestamp: number;
}
function LiveMetrics() {
const [metrics, setMetrics] = useState<Metrics[]>([]);
const [latest, setLatest] = useState<Metrics | null>(null);
const { status } = useWebSocket({
url: 'wss://monitoring.example.com/metrics',
onMessage: (data) => {
if (data.type === 'metrics') {
const metric: Metrics = data.payload;
setLatest(metric);
// Behalte nur letzte 60 Datenpunkte (für Chart)
setMetrics(prev => {
const updated = [...prev, metric];
return updated.slice(-60);
});
}
}
});
return (
<div className="dashboard">
<div className="status">
{status === WebSocketStatus.CONNECTED ? '🟢 Live' : '🔴 Disconnected'}
</div>
{latest && (
<div className="metrics-grid">
<MetricCard
title="CPU Usage"
value={`${latest.cpu.toFixed(1)}%`}
trend={calculateTrend(metrics, 'cpu')}
/>
<MetricCard
title="Memory"
value={`${latest.memory.toFixed(0)} MB`}
trend={calculateTrend(metrics, 'memory')}
/>
<MetricCard
title="Requests/min"
value={latest.requests.toString()}
trend={calculateTrend(metrics, 'requests')}
/>
<MetricCard
title="Errors"
value={latest.errors.toString()}
trend={calculateTrend(metrics, 'errors')}
/>
</div>
)}
<div className="chart">
<LineChart data={metrics} />
</div>
</div>
);
}
function calculateTrend(
metrics: Metrics[],
key: keyof Omit<Metrics, 'timestamp'>
): 'up' | 'down' | 'stable' {
if (metrics.length < 2) return 'stable';
const current = metrics[metrics.length - 1][key];
const previous = metrics[metrics.length - 2][key];
if (current > previous * 1.1) return 'up';
if (current < previous * 0.9) return 'down';
return 'stable';
}
Problem: 100 Messages/Sekunde → 100 Re-Renders/Sekunde → UI stockt.
import { useTransition, useState } from 'react';
import { useWebSocket } from './hooks/useWebSocket';
function HighFrequencyFeed() {
const [messages, setMessages] = useState<Message[]>([]);
const [isPending, startTransition] = useTransition();
useWebSocket({
url: 'wss://api.example.com/feed',
onMessage: (data) => {
// Update ist non-urgent, nicht blockieren
startTransition(() => {
setMessages(prev => [data, ...prev].slice(0, 100));
});
}
});
return (
<div>
{isPending && <div className="updating-indicator">Updating...</div>}
<div className="feed">
{messages.map(msg => (
<div key={msg.id}>{msg.content}</div>
))}
</div>
</div>
);
}
startTransition markiert Update als low-priority. React
kann dringende Updates (User-Input) priorisieren.
Alternativ: Batching mit useDeferredValue
import { useDeferredValue, useState } from 'react';
function HighFrequencyFeed() {
const [messages, setMessages] = useState<Message[]>([]);
const deferredMessages = useDeferredValue(messages);
useWebSocket({
url: 'wss://api.example.com/feed',
onMessage: (data) => {
setMessages(prev => [data, ...prev].slice(0, 100));
}
});
// UI rendert deferredMessages (verzögert)
// State updates schnell, Rendering delayed
return (
<div className="feed">
{deferredMessages.map(msg => (
<div key={msg.id}>{msg.content}</div>
))}
</div>
);
}
Socket.io abstrahiert WebSockets und bietet Fallbacks (Long-Polling), Auto-Reconnect, Rooms.
npm install socket.io-client// hooks/useSocketIO.ts
import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
export function useSocketIO(url: string) {
const [socket, setSocket] = useState<Socket | null>(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
const socketInstance = io(url, {
transports: ['websocket', 'polling'], // WebSocket bevorzugt
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 1000
});
socketInstance.on('connect', () => {
console.log('Socket.io connected');
setConnected(true);
});
socketInstance.on('disconnect', () => {
console.log('Socket.io disconnected');
setConnected(false);
});
setSocket(socketInstance);
return () => {
socketInstance.disconnect();
};
}, [url]);
return { socket, connected };
}
// ChatRoom.tsx mit Socket.io
import { useEffect, useState } from 'react';
import { useSocketIO } from './hooks/useSocketIO';
function ChatRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<Message[]>([]);
const { socket, connected } = useSocketIO('https://chat.example.com');
useEffect(() => {
if (!socket) return;
// Join Room
socket.emit('join', { roomId });
// Listen for Messages
socket.on('message', (message: Message) => {
setMessages(prev => [...prev, message]);
});
socket.on('history', (history: Message[]) => {
setMessages(history);
});
return () => {
socket.off('message');
socket.off('history');
socket.emit('leave', { roomId });
};
}, [socket, roomId]);
const sendMessage = (text: string) => {
socket?.emit('message', { roomId, text });
};
return (
<div>
<div>Status: {connected ? 'Connected' : 'Disconnected'}</div>
{/* ... Messages UI */}
</div>
);
}
Socket.io Vorteile: - Auto-Reconnect out-of-the-box - Fallback zu Long-Polling wenn WebSocket blockiert - Rooms & Namespaces - Acknowledgements (Message-Bestätigung) - Binary Support
Nachteile: - Größerer Bundle (~30KB) - Nicht Standard-WebSocket-Protokoll - Server muss Socket.io sprechen
| Feature | Native WebSocket | Socket.io |
|---|---|---|
| Bundle Size | ~0KB | ~30KB |
| Auto-Reconnect | Manual | Built-in |
| Fallbacks | Keine | Long-Polling |
| Binary Data | Ja | Ja |
| Rooms | Manual | Built-in |
| Server-Library | Any | socket.io (Node.js) |
Für app-weite WebSocket-Verbindung: Context statt Hook in jeder Komponente.
// contexts/WebSocketContext.tsx
import { createContext, useContext, ReactNode } from 'react';
import { useWebSocket, WebSocketStatus } from '../hooks/useWebSocket';
interface WebSocketContextType {
status: WebSocketStatus;
send: (data: any) => void;
lastMessage: any;
}
const WebSocketContext = createContext<WebSocketContextType | undefined>(undefined);
export function WebSocketProvider({
children,
url
}: {
children: ReactNode;
url: string;
}) {
const { status, send, lastMessage } = useWebSocket({ url });
return (
<WebSocketContext.Provider value={{ status, send, lastMessage }}>
{children}
</WebSocketContext.Provider>
);
}
export function useWebSocketContext() {
const context = useContext(WebSocketContext);
if (!context) {
throw new Error('useWebSocketContext must be used within WebSocketProvider');
}
return context;
}
// App.tsx
import { WebSocketProvider } from './contexts/WebSocketContext';
function App() {
return (
<WebSocketProvider url="wss://api.example.com">
<Dashboard />
<Notifications />
<Chat />
</WebSocketProvider>
);
}
// Alle Komponenten teilen eine WebSocket-Verbindung
function Dashboard() {
const { status, lastMessage } = useWebSocketContext();
useEffect(() => {
if (lastMessage?.type === 'metrics') {
// Handle metrics
}
}, [lastMessage]);
return <div>Status: {status}</div>;
}
Fehler 1: WebSocket in useEffect ohne Cleanup
// ❌ Falsch: Memory Leak!
useEffect(() => {
const ws = new WebSocket('wss://...');
ws.onmessage = (e) => setData(JSON.parse(e.data));
// WebSocket wird nie geschlossen!
}, []);
// ✓ Richtig: Cleanup
useEffect(() => {
const ws = new WebSocket('wss://...');
ws.onmessage = (e) => setData(JSON.parse(e.data));
return () => ws.close();
}, []);
Fehler 2: Send vor Connection Open
// ❌ Falsch: Send sofort nach new WebSocket
const ws = new WebSocket('wss://...');
ws.send(JSON.stringify({ type: 'hello' })); // Error: Not yet open!
// ✓ Richtig: Warten auf onopen
const ws = new WebSocket('wss://...');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'hello' }));
};
Fehler 3: State-Updates aus alten Messages
// ❌ Falsch: Race Condition bei Reconnect
useEffect(() => {
const ws = new WebSocket('wss://...');
ws.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
return () => ws.close();
}, [dependency]); // Bei Änderung: neue WebSocket, alte empfängt noch Messages!
// ✓ Richtig: Cleanup vor neuem Setup
useEffect(() => {
let ws: WebSocket | null = null;
let mounted = true;
ws = new WebSocket('wss://...');
ws.onmessage = (event) => {
if (mounted) { // Nur wenn noch mounted
setMessages(prev => [...prev, JSON.parse(event.data)]);
}
};
return () => {
mounted = false;
ws?.close();
};
}, [dependency]);
Fehler 4: Keine Error-Handling
// ❌ Falsch: Kein Error-Handling
ws.onmessage = (event) => {
const data = JSON.parse(event.data); // Was wenn kein JSON?
setData(data.payload.value); // Was wenn Struktur anders?
};
// ✓ Richtig: Try-Catch + Validation
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (isValidMessage(data)) {
setData(data.payload.value);
} else {
console.error('Invalid message format:', data);
}
} catch (error) {
console.error('Failed to parse message:', error);
}
};
Fehler 5: Unbegrenzte Message-Arrays
// ❌ Falsch: Array wächst unbegrenzt
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
// Nach 10.000 Messages: Memory-Problem!
};
// ✓ Richtig: Limit setzen
ws.onmessage = (event) => {
setMessages(prev => {
const updated = [...prev, event.data];
return updated.slice(-100); // Nur letzte 100
});
};
// __tests__/ChatRoom.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ChatRoom } from '../ChatRoom';
import WS from 'jest-websocket-mock';
describe('ChatRoom', () => {
let server: WS;
beforeEach(() => {
server = new WS('ws://localhost:8080');
});
afterEach(() => {
WS.clean();
});
test('connects to WebSocket on mount', async () => {
render(<ChatRoom roomId="test" />);
await server.connected;
expect(server).toHaveReceivedMessages([
JSON.stringify({ type: 'join', roomId: 'test' })
]);
});
test('displays received messages', async () => {
render(<ChatRoom roomId="test" />);
await server.connected;
server.send(JSON.stringify({
type: 'message',
message: {
id: '1',
userId: '2',
username: 'Bob',
text: 'Hello!',
timestamp: Date.now()
}
}));
await waitFor(() => {
expect(screen.getByText('Hello!')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
});
});
test('sends message on form submit', async () => {
const user = userEvent.setup();
render(<ChatRoom roomId="test" userId="1" username="Alice" />);
await server.connected;
const input = screen.getByPlaceholderText('Type a message...');
await user.type(input, 'Test message');
await user.click(screen.getByText('Send'));
await waitFor(() => {
expect(server).toHaveReceivedMessages([
expect.stringContaining('"text":"Test message"')
]);
});
});
});
WebSockets transformieren React-Apps von Request-Response zu Event-Driven. Chat, Live-Dashboards, Collaborative Tools – alles braucht Echtzeitdaten. Mit custom Hooks, TypeScript-Types, Reconnect-Logic und React 18 Transitions wird WebSocket-Integration robust, typsicher und performant.