WebSocket und Mercure integrieren
Polling ist keine Echtzeit-Architektur – es ist ein Kompromiss, der unter Last zusammenbricht. WebSocket und Mercure bieten echte Push-Kommunikation, aber ihre Integration in React erfordert durchdachte Custom Hooks, robuste Reconnect-Logik und sicheres State-Management für Echtzeit-Datenströme.
Inhaltsverzeichnis
- 1. WebSocket vs. SSE vs. Long-Polling: die richtige Wahl
- 2. useWebSocket: ein robuster Custom Hook
- 3. Reconnect-Logik mit Exponential Backoff
- 4. Mercure: Server-Sent Events mit Hub
- 5. useMercure: Topics und Subscriptions in React
- 6. Authentifizierung: JWT und Mercure Authorization
- 7. State-Management für Echtzeit-Datenströme
- 8. Performance: Verbindungen optimieren
- 9. WebSocket vs. Mercure im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. WebSocket vs. SSE vs. Long-Polling: die richtige Wahl
Die Entscheidung zwischen WebSocket, Server-Sent Events (SSE) und Long-Polling hängt von der Kommunikationsrichtung und den Infrastrukturanforderungen ab. WebSocket ist ein bidirektionales, vollduplexes Protokoll – der Client kann jederzeit Nachrichten senden und empfangen. Das macht es ideal für Chat-Anwendungen, kollaborative Tools und Gaming, wo beide Seiten aktiv kommunizieren. WebSocket erfordert jedoch einen speziellen Server, der persistente Verbindungen verwaltet, und funktioniert nicht über alle Proxy-Konfigurationen hinweg zuverlässig.
Server-Sent Events sind unidirektional: Der Server pusht Daten, der Client kann nur über normale HTTP-Requests antworten. Das klingt nach einem Nachteil, ist aber für die meisten Echtzeit-Use-Cases ausreichend: Benachrichtigungen, Live-Updates, Dashboards, Bestellstatus-Tracking. SSE nutzt normales HTTP und funktioniert daher durch alle Proxies und Load-Balancer ohne spezielle Konfiguration. Mercure baut auf SSE auf und ergänzt es um ein vollständiges Hub-Konzept mit Topic-basiertem Routing und JWT-Authentifizierung.
2. useWebSocket: ein robuster Custom Hook
Die naivste WebSocket-Implementierung in React erstellt die Verbindung direkt in einem useEffect und vergisst dabei, die Verbindung beim Unmount zu schließen, bei Netzwerkfehlern neu zu verbinden oder mehrfache Verbindungen durch StrictMode-doppelte Mounts zu verhindern. Ein robuster useWebSocket-Hook kapselt all diese Logik und stellt der Komponente nur eine saubere API bereit: Verbindungsstatus, empfangene Nachrichten und eine Send-Funktion.
Der Hook muss mit React StrictMode kompatibel sein, der im Development-Modus jeden Effect zweimal ausführt. Das bedeutet, die WebSocket-Verbindung wird geöffnet, sofort wieder geschlossen und dann erneut geöffnet. Die Lösung ist ein Cleanup in der Effect-Return-Funktion und eine Referenz auf die aktuelle Verbindungsinstanz. useRef hält die WebSocket-Instanz, ohne einen Re-Render auszulösen. useState hält nur die für das UI relevanten Daten: Verbindungsstatus und letzte Nachricht.
// useWebSocket.ts — Robust WebSocket hook with cleanup and status tracking
import { useCallback, useEffect, useRef, useState } from 'react';
type WsStatus = 'connecting' | 'open' | 'closed' | 'error';
interface UseWebSocketReturn {
status: WsStatus;
lastMessage: MessageEvent | null;
send: (data: string | ArrayBuffer | Blob) => void;
}
export function useWebSocket(url: string): UseWebSocketReturn {
const wsRef = useRef<WebSocket | null>(null);
const [status, setStatus] = useState<WsStatus>('connecting');
const [lastMessage, setLastMessage] = useState<MessageEvent | null>(null);
useEffect(() => {
const ws = new WebSocket(url);
wsRef.current = ws;
setStatus('connecting');
ws.onopen = () => setStatus('open');
ws.onmessage = (event) => setLastMessage(event);
ws.onerror = () => setStatus('error');
ws.onclose = () => setStatus('closed');
// Cleanup: close connection when component unmounts or URL changes
return () => {
ws.close(1000, 'Component unmounted');
wsRef.current = null;
};
}, [url]);
const send = useCallback((data: string | ArrayBuffer | Blob) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(data);
} else {
console.warn('[WS] Cannot send: connection not open');
}
}, []);
return { status, lastMessage, send };
}
3. Reconnect-Logik mit Exponential Backoff
Ein WebSocket, der nach einem Verbindungsabbruch nicht automatisch wiederhergestellt wird, ist für Produktionsanwendungen unbrauchbar. Netzwerkschwankungen, Server-Neustarts und Timeouts sind normal – die App muss damit umgehen. Die naive Lösung ist ein sofortiger Reconnect nach onclose, was bei einem dauerhaften Serverausfall zu Tausenden von Verbindungsversuchen pro Minute führt und den Server weiter belastet.
Die korrekte Lösung ist Exponential Backoff: Der erste Reconnect erfolgt nach 500ms, der nächste nach 1s, dann 2s, 4s, bis zu einem Maximum von 30s. Eine optionale Jitter-Komponente (±20% zufällige Variation) verhindert den sogenannten Thundering-Herd-Effekt, bei dem viele Clients gleichzeitig reconnecten und den Server überlasten. Nach einer erfolgreichen Verbindung wird der Backoff-Zähler zurückgesetzt. Der Reconnect sollte aufhören, wenn die Komponente unmountet oder der Nutzer die Verbindung explizit getrennt hat.
4. Mercure: Server-Sent Events mit Hub
Mercure ist ein offenes Protokoll und Hub-Server, der SSE um Topic-basiertes Pub/Sub erweitert. Statt einer einzelnen Event-Stream-URL für alles können Publisher gezielt Topics veröffentlichen, auf die Subscriber zuhören. Das ermöglicht granulare Subscriptions: Ein Nutzer abonniert nur Benachrichtigungen für seine eigenen Bestellungen, nicht alle Bestellungen im System. Der Mercure-Hub übernimmt das Routing – die Applikation veröffentlicht eine HTTP-POST-Anfrage an den Hub, und der Hub leitet sie an alle passenden Subscriber weiter.
Mercure ist in Caddy integriert und kann als eigenständiger Hub-Server oder als Caddy-Modul betrieben werden. Symfony hat eine native Mercure-Integration, aber der Hub funktioniert mit jeder Sprache und jedem Framework, das HTTP-Requests senden kann. Die Subscriber-Verbindung ist ein normaler EventSource-Request mit dem Topic als Query-Parameter. Für private Topics wird ein JWT-Token verwendet, das die erlaubten Topics im Payload enthält.
// useMercure.ts — Subscribe to Mercure topics via EventSource
import { useEffect, useRef, useState } from 'react';
interface MercureOptions {
hubUrl: string;
topics: string[];
token?: string; // JWT for private topics
}
interface UseMercureReturn<T> {
data: T | null;
error: Event | null;
connected: boolean;
}
export function useMercure<T = unknown>({
hubUrl,
topics,
token,
}: MercureOptions): UseMercureReturn<T> {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Event | null>(null);
const [connected, setConnected] = useState(false);
const esRef = useRef<EventSource | null>(null);
useEffect(() => {
// Build URL with topic query params
const url = new URL(hubUrl);
topics.forEach((t) => url.searchParams.append('topic', t));
// Attach JWT as cookie or URL param (hub-dependent)
const init: EventSourceInit = {};
if (token) {
// Some hubs accept token as URL param, others via cookie
url.searchParams.set('authorization', token);
}
const es = new EventSource(url.toString(), init);
esRef.current = es;
es.onopen = () => setConnected(true);
es.onerror = (e) => { setError(e); setConnected(false); };
es.onmessage = (e) => {
try {
setData(JSON.parse(e.data) as T);
} catch {
setData(e.data as unknown as T);
}
};
return () => { es.close(); esRef.current = null; setConnected(false); };
}, [hubUrl, topics.join(','), token]);
return { data, error, connected };
}
5. useMercure: Topics und Subscriptions in React
Die praktische Nutzung von useMercure in Komponenten folgt demselben Muster wie jeder Custom Hook: Der Hook wird auf der obersten Ebene der Komponente aufgerufen und liefert reaktive Daten. Das Besondere bei Mercure ist das Topic-Konzept: Ein Topic ist eine URI, die das Thema einer Nachricht beschreibt. Für Bestellupdates könnte das Topic https://mironsoft.de/orders/42 sein – genau die Bestellung des angemeldeten Nutzers.
Wenn Topics dynamisch sind (abhängig von Props oder State), müssen sie korrekt als Dependency des Hooks übergeben werden. Eine häufige Falle ist das direkte Übergeben eines Arrays-Literals als Topic-Prop, das bei jedem Render eine neue Referenz erzeugt und den Effect neu auslöst. Die Lösung ist useMemo für das Topics-Array oder stabile String-Darstellungen. Für TypeScript-Projekte lohnt sich eine generische Hook-Variante, die den Datentyp der erwarteten Nachrichten als Typ-Parameter nimmt.
6. Authentifizierung: JWT und Mercure Authorization
Private Mercure-Topics erfordern JWT-Authentifizierung. Das JWT enthält einen mercure-Claim mit den erlaubten Topics. Der Mercure-Hub prüft das Token und erlaubt oder verweigert die Subscription. Das Token wird üblicherweise vom Backend beim Login oder auf Anfrage ausgestellt und hat eine kurze Lebenszeit. Ein kritischer Punkt: Das Token muss vor Ablauf erneuert werden, da eine bestehende EventSource-Verbindung den Token nicht automatisch erneuert.
Für WebSocket-Authentifizierung gibt es zwei gängige Ansätze: Token im Query-Parameter bei der Verbindungsherstellung (weniger sicher, da in Logs sichtbar) oder Token in der ersten Nachricht nach dem Handshake. Letzteres erfordert ein Server-seitiges Protokoll, das die erste Nachricht als Authentifizierungs-Frame behandelt. Für Mercure mit HTTP-Cookie-basierter Auth sendet der Browser den Cookie automatisch mit, was die sicherste und einfachste Variante ist.
7. State-Management für Echtzeit-Datenströme
Echtzeit-Datenströme stellen besondere Anforderungen an das State-Management. Ein einzelner WebSocket-Event kann viele Komponenten betreffen – ein Bestellstatus-Update betrifft die Bestellliste, die Bestelldetailseite und möglicherweise einen Notification-Badge. Das naive Muster, den WebSocket-State in jeder Komponente separat zu halten, führt zu Inkonsistenzen und mehrfachen Verbindungen. Die Lösung ist ein zentrales Store-Konzept.
Mit Zustand oder Jotai lässt sich ein globaler Echtzeit-Store aufbauen, der von einem einzigen useWebSocket-Hook oder useMercure-Hook befüllt wird. Komponenten subscriben nur auf die für sie relevanten Teile des Stores und werden nur dann re-gerendert, wenn diese sich ändern. Bei hochfrequenten Updates (z. B. Kurs-Ticker) muss throttling eingesetzt werden: React sollte nicht 60 State-Updates pro Sekunde verarbeiten, wenn der Screen nur 60 Frames pro Sekunde darstellt. useTransition oder manuelles Batching mit flushSync helfen, die Update-Frequenz zu kontrollieren.
// realtimeStore.ts — Zustand store fed by WebSocket events
import { create } from 'zustand';
interface OrderUpdate {
id: string;
status: 'pending' | 'processing' | 'shipped' | 'delivered';
updatedAt: string;
}
interface RealtimeState {
orders: Record<string, OrderUpdate>;
notifications: string[];
updateOrder: (update: OrderUpdate) => void;
addNotification: (message: string) => void;
}
export const useRealtimeStore = create<RealtimeState>((set) => ({
orders: {},
notifications: [],
updateOrder: (update) =>
set((state) => ({
orders: { ...state.orders, [update.id]: update },
})),
addNotification: (message) =>
set((state) => ({
notifications: [...state.notifications.slice(-49), message],
})),
}));
// Component: subscribe to Mercure and feed the store
function RealtimeProvider({ children }: { children: React.ReactNode }) {
const updateOrder = useRealtimeStore((s) => s.updateOrder);
const { data } = useMercure<OrderUpdate>({
hubUrl: 'https://mercure.mironsoft.de/.well-known/mercure',
topics: ['https://mironsoft.de/orders'],
});
useEffect(() => {
if (data) updateOrder(data);
}, [data, updateOrder]);
return <>{children}</>;
}
8. Performance: Verbindungen optimieren
Jede WebSocket- oder SSE-Verbindung belegt Ressourcen auf Server und Client. Eine häufige Performance-Falle in React-Apps: Mehrere Komponenten öffnen jeweils eine eigene Verbindung zu demselben Endpoint. Das Ergebnis sind zehn parallele WebSocket-Verbindungen für zehn offene Tabs – zehn Mal der Server-Overhead für denselben Datenstrom. Die Lösung ist das Singleton-Pattern für Verbindungen: Ein einzelner Connection-Manager auf App-Ebene verwaltet alle Verbindungen und stellt sie per Context oder Store bereit.
Für EventSource-basierte SSE-Verbindungen gibt es eine weitere Optimierung: Die EventSource-API des Browsers cached Verbindungen automatisch, wenn mehrere Requests zur selben URL gesendet werden – aber nur innerhalb desselben Browsing-Contexts. Für WebSocket-Multiplexing bieten Bibliotheken wie socket.io Namespace-Konzepte, die mehrere logische Kanäle über eine physische Verbindung leiten. Bei Mercure reicht ein einziger EventSource mit mehreren Topics als Query-Parameter.
9. WebSocket vs. Mercure im direkten Vergleich
Die Wahl zwischen WebSocket und Mercure ist keine Frage der Performance, sondern der Architektur. Beide Technologien haben unterschiedliche Stärken, und die richtige Wahl hängt vom spezifischen Use-Case ab.
| Kriterium | WebSocket | Mercure (SSE) | Empfehlung |
|---|---|---|---|
| Kommunikationsrichtung | Bidirektional | Unidirektional (Server → Client) | WS für Chat/Gaming, Mercure für Notifications |
| Proxy-Kompatibilität | Problematisch | Problemlos (HTTP) | Mercure in Unternehmensnetzen bevorzugen |
| Authentifizierung | Manuell implementieren | JWT-nativ im Hub | Mercure für private Topics |
| Topic-Routing | Anwendungsschicht | Hub übernimmt Routing | Mercure für Multi-Topic-Szenarien |
| Serverinfrastruktur | WebSocket-fähiger Server nötig | Standard HTTP-Server genügt | Mercure einfacher zu deployen |
In modernen Projekten werden WebSocket und Mercure oft kombiniert: Mercure für Push-Notifications und Status-Updates, WebSocket für interaktive Echtzeit-Features wie kollaborative Bearbeitung. Das Backend veröffentlicht Events an den Mercure-Hub per HTTP-POST – das funktioniert mit jedem Backend-Framework und erfordert keine dauerhafte Verbindungsverwaltung auf Serverseite.
Mironsoft
React Realtime · WebSocket · Mercure · Echtzeit-Architektur
Echtzeit-Features für eure React-App?
Wir entwerfen und implementieren Echtzeit-Architekturen mit WebSocket und Mercure – von Custom Hooks über Reconnect-Logik bis zur skalierbaren Hub-Infrastruktur.
Architektur-Design
WebSocket vs. Mercure – die richtige Wahl für euren Use-Case
Hook-Entwicklung
Robuste Custom Hooks mit Reconnect, Auth und State-Management
Hub-Setup
Mercure Hub mit Caddy aufsetzen und in bestehende Backend-Infrastruktur integrieren
10. Zusammenfassung
WebSocket und Mercure lösen dasselbe Grundproblem auf unterschiedliche Weise: Echtzeit-Push von Server zu Client ohne Polling. WebSocket ist die richtige Wahl für bidirektionale, interaktive Features. Mercure ist die bessere Wahl für unidirektionale Notifications, Status-Updates und Events, weil es auf Standard-HTTP aufbaut, Topic-Routing übernimmt und JWT-Authentifizierung nativ unterstützt. In React kapselt man beide in Custom Hooks, die Verbindungsstatus, Reconnect-Logik und State-Management intern verwalten und der Komponente eine saubere API bereitstellen.
State-Management für Echtzeit-Daten gehört in einen zentralen Store, nicht in lokale Komponenten-States. Singleton-Verbindungen verhindern mehrfache parallele Connections. Throttling und Batching schützen vor übermäßigen Re-Renders bei hochfrequenten Updates. Mit diesen Bausteinen lassen sich Echtzeit-Features bauen, die auch unter Last zuverlässig funktionieren.
React Realtime mit WebSocket und Mercure — Das Wichtigste auf einen Blick
Hook-Design
useWebSocket und useMercure kapseln Verbindung, Status und Reconnect intern. Komponenten bekommen nur saubere Daten und eine Send-Funktion.
Reconnect
Exponential Backoff mit Jitter verhindert Thundering Herd. Backoff zurücksetzen nach erfolgreicher Verbindung. Reconnect stoppen bei explizitem Disconnect.
State-Management
Zentraler Store (Zustand/Jotai) statt lokalem State pro Komponente. Ein einziger Hook befüllt den Store, viele Komponenten lesen daraus.
WebSocket vs. Mercure
WebSocket für bidirektionale Features, Mercure für Push-Notifications. Mercure ist proxy-freundlicher und hat JWT-Auth nativ. Kombinieren ist valide.