21 useSyncExternalStore - Externe Stores sicher integrieren

Moderne Webanwendungen sind selten isolierte Systeme. Sie interagieren mit Browser-APIs, nutzen Zustandsmanagement-Bibliotheken oder greifen auf andere externe Datenquellen zu, die außerhalb von Reacts direkter Kontrolle stehen. Hier kommt der useSyncExternalStore Hook ins Spiel, der speziell dafür entwickelt wurde, diese externen Datenquellen sicher und konsistent mit React zu synchronisieren.

Dieser Hook wurde mit React 18 eingeführt und löst ein fundamentales Problem, das lange Zeit die React-Community beschäftigt hat: das sogenannte “Tearing”-Problem. Tearing tritt auf, wenn verschiedene Teile der Benutzeroberfläche gleichzeitig unterschiedliche Zustände derselben externen Datenquelle anzeigen, was zu visuell inkonsistenten und verwirrenden Benutzererfahrungen führen kann.

21.1 Das Problem mit externen Datenquellen

Bevor wir verstehen, warum useSyncExternalStore notwendig ist, sollten wir das zugrundeliegende Problem betrachten. Stellen Sie sich vor, Sie möchten den Online-Status des Browsers in Ihrer React-Anwendung anzeigen. Ein naiver Ansatz könnte die Verwendung von useEffect in Kombination mit useState sein.

Der problematische Ansatz würde so aussehen: Sie registrieren Event-Listener für die online und offline Events und aktualisieren einen lokalen State entsprechend. Auf den ersten Blick scheint das zu funktionieren, aber bei genauerer Betrachtung offenbaren sich Schwächen. React kann in verschiedenen Modi arbeiten, insbesondere im Concurrent Rendering, wo Updates unterbrochen und später fortgesetzt werden können. In diesem Szenario kann es passieren, dass eine Komponente den alten Zustand liest, während eine andere bereits den neuen Zustand sieht.

Diese Inkonsistenz wird als Tearing bezeichnet und kann besonders problematisch werden, wenn schnelle Änderungen in der externen Datenquelle auftreten. Stellen Sie sich eine Anwendung vor, die den Verbindungsstatus anzeigt und basierend darauf verschiedene UI-Elemente ein- oder ausblendet. Wenn diese Elemente inkonsistente Zustände anzeigen, wird die Benutzererfahrung erheblich beeinträchtigt.

21.2 Die Anatomie von useSyncExternalStore

Der useSyncExternalStore Hook folgt einem bewährten Muster aus der Softwarearchitektur: dem Observer-Pattern. Er benötigt drei Funktionen, die gemeinsam eine sichere Brücke zwischen der externen Datenquelle und React bilden.

Die erste Funktion ist die Subscribe-Funktion. Diese erhält einen Callback-Parameter und ist dafür verantwortlich, diesen Callback zu registrieren, damit er aufgerufen wird, wenn sich die externe Datenquelle ändert. Gleichzeitig muss sie eine Cleanup-Funktion zurückgeben, die die Registrierung wieder aufhebt. Diese Architektur stellt sicher, dass keine Memory-Leaks entstehen und dass React ordnungsgemäß über Änderungen informiert wird.

Die zweite Funktion ist die Snapshot-Funktion, die den aktuellen Wert der externen Datenquelle zurückliefert. Diese Funktion wird von React aufgerufen, wann immer der aktuelle Zustand benötigt wird. Besonders wichtig ist hierbei, dass diese Funktion bei identischen Zuständen auch identische Referenzen zurückgeben sollte, um unnötige Re-Renders zu vermeiden.

Die dritte Funktion ist die Server-Snapshot-Funktion, die speziell für Server-Side Rendering entwickelt wurde. Da externe Browser-APIs auf dem Server nicht verfügbar sind, benötigt React einen Fallback-Wert, der während des Server-Renderings verwendet werden kann.

21.3 Praktische Implementierung und bewährte Patterns

Bei der Implementierung von useSyncExternalStore sollten Sie mehrere bewährte Patterns befolgen. Die Subscribe-Funktion sollte mit useCallback memoiziert werden, um zu verhindern, dass sich bei jedem Render eine neue Funktion-Instanz bildet, was zu unnötigen Re-Subscriptions führen würde.

Wenn Sie mit komplexen Datentypen arbeiten, wird die Snapshot-Funktion zu einer besonderen Herausforderung. Objekte müssen bei gleichen Inhalten identische Referenzen haben, um Reacts Vergleichslogik zu unterstützen. Eine bewährte Lösung ist die Serialisierung des Objekts zu einem String mittels JSON.stringify. Dies gewährleistet, dass strukturell identische Objekte als gleich erkannt werden.

Ein häufiger Fehler beim Umgang mit useSyncExternalStore ist die Implementierung einer instabilen Snapshot-Funktion. Wenn diese Funktion bei jedem Aufruf neue Objekt-Instanzen erstellt, auch wenn die Daten unverändert sind, führt dies zu endlosen Re-Render-Zyklen. React vergleicht die Rückgabewerte der Snapshot-Funktion mit Object.is, daher ist Referenz-Stabilität entscheidend.

21.4 Integration mit bestehenden Systemen

Der useSyncExternalStore Hook zeigt seine wahre Stärke bei der Integration mit bestehenden Systemen. Browser-APIs wie localStorage, sessionStorage, oder das navigator-Objekt sind klassische Kandidaten für diese Integration. Auch moderne Zustandsmanagement-Bibliotheken nutzen diesen Hook intern, um sich sicher mit React zu synchronisieren.

Bei der Arbeit mit localStorage beispielsweise können Sie eine Store-Klasse erstellen, die das Subscribe/Notify-Pattern implementiert. Diese Klasse kapselt alle Zugriffe auf localStorage und stellt eine konsistente API für React-Komponenten bereit. Mehrere Komponenten können sich bei derselben Store-Instanz registrieren und bleiben automatisch synchron, auch wenn Änderungen von außerhalb von React kommen.

Ein besonders interessanter Anwendungsfall ist die Verfolgung von Browser-Events wie Fenstergröße oder Orientierung. Diese Informationen ändern sich unabhängig von React und müssen dennoch konsistent in der gesamten Anwendung verfügbar sein. Der useSyncExternalStore Hook macht diese Integration trivial und sicher.

21.5 Performance-Überlegungen und Optimierungen

Performance ist ein kritischer Aspekt bei der Verwendung von useSyncExternalStore. Da die Snapshot-Funktion bei jeder Änderung der externen Datenquelle aufgerufen wird, sollte sie so effizient wie möglich sein. Vermeiden Sie teure Berechnungen in dieser Funktion und lagern Sie diese bei Bedarf in die Subscribe-Logik aus.

Die Subscribe-Funktion wird nur einmal pro Komponenten-Mount aufgerufen, daher ist sie der ideale Ort für aufwendigere Setup-Operationen. Stellen Sie jedoch sicher, dass die entsprechenden Cleanup-Operationen in der zurückgegebenen Funktion ordnungsgemäß implementiert sind.

Ein weiterer Performance-Aspekt betrifft die Granularität Ihrer externen Stores. Anstatt einen monolithischen Store zu erstellen, der alle Daten enthält, sollten Sie kleinere, fokussierte Stores bevorzugen. Dies reduziert die Anzahl der Komponenten, die bei Änderungen neu gerendert werden müssen.

21.6 Troubleshooting

Ein typischer Fehler ist der Versuch, useSyncExternalStore für Daten zu verwenden, die bereits von React verwaltet werden. Dieser Hook ist ausschließlich für externe Datenquellen gedacht. Für React-eigene Daten sollten Sie weiterhin useState, useReducer oder Context verwenden.

Ein weiterer häufiger Fehler liegt in der unvollständigen Implementierung der Cleanup-Logik. Vergessen Sie nicht, alle Event-Listener, Timer oder andere Ressourcen in der von der Subscribe-Funktion zurückgegebenen Cleanup-Funktion zu entfernen. Andernfalls entstehen Memory-Leaks, die die Performance Ihrer Anwendung beeinträchtigen können.

Bei der Arbeit mit Server-Side Rendering ist es entscheidend, dass die Server-Snapshot-Funktion einen sinnvollen Default-Wert liefert. Dieser sollte so gewählt werden, dass die initiale Server-Ausgabe nicht zu stark von der späteren Client-Hydration abweicht, um Hydration-Fehler zu vermeiden.