36 WebSockets – Echtzeitdaten in React

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.

36.1 Das Problem: Polling vs. WebSockets

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)

36.2 Native WebSocket API: Der Grundaufbau

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.

36.3 Custom Hook: useWebSocket für wiederverwendbare Logic

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

36.4 Verwendung: Chat-Komponente

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

36.5 TypeScript: Typsichere Messages

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, /* ... */ };
}

36.6 Reconnect-Strategien: Exponential Backoff

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)

36.7 Heartbeat: Connection Keep-Alive

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

36.8 Live Dashboard: Real-time Metrics

// 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';
}

36.9 React 18 Transitions: Smooth Updates bei vielen Messages

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

36.10 Socket.io: Higher-Level Alternative

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)

36.11 Context Pattern: Global WebSocket State

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

36.12 Häufige Fehler

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

36.13 Testing: WebSocket-Komponenten testen

// __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.