</>
{ }
React · WebSocket · Mercure · Realtime
React Realtime:
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.

16 Min. Lesezeit WebSocket · Mercure · SSE · Custom Hooks · Reconnect · Authentifizierung React 18+ · TypeScript · Mercure Hub

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.

11. FAQ: React Realtime mit WebSocket und Mercure

1WebSocket vs. Server-Sent Events?
WebSocket bidirektional, SSE unidirektional (Server → Client). SSE ist HTTP-basiert und proxy-freundlicher. WebSocket für Chat/Gaming, SSE für Notifications.
2Was ist Mercure?
SSE + Topic-Routing + JWT-Auth. Hub nimmt HTTP-POST vom Publisher, liefert per EventSource an Subscriber. Caddy-Modul oder eigenständiger Server.
3Mehrfache Verbindungen verhindern?
Singleton-Connection-Manager per Context oder globalem Store. Komponenten lesen aus Store, öffnen keine eigene Verbindung.
4Exponential Backoff?
Wartezeit exponentiell erhöhen: 500ms → 1s → 2s → max 30s. Jitter ±20% verhindert Thundering Herd. Nach Erfolg Zähler zurücksetzen.
5Private Mercure-Topics authentifizieren?
JWT mit mercure-Claim vom Backend ausstellen. Per Cookie (sicher) oder URL-Parameter an Hub übergeben. Hub prüft erlaubte Topics.
6Hochfrequente Updates in React?
Throttling und Batching. useTransition priorisiert. Zustand-Store mit selektiven Subscriptions verhindert unnötige Re-Renders.
7StrictMode-Problem mit WebSocket?
StrictMode führt Effects doppelt aus. Cleanup in useEffect-Return schließt Verbindung korrekt. wsRef verhindert State-Updates auf alten Verbindungen.
8WebSocket-Hooks testen?
MSW v2 mit nativem WebSocket-Support mocken. Für Integrationstests lokalen WS-Server mit ws-Paket starten.
9Mercure ohne Symfony?
Ja. Jedes Backend kann HTTP-POST an den Hub senden. Mercure Hub läuft als Caddy-Modul oder eigenständiger Go-Server.
10WebSocket und Mercure kombinieren?
Valides Muster: WS für interaktive Features, Mercure für Push-Notifications. Beide können denselben Zustand-Store befüllen.