useSyncExternalStore abonniert für eine Komponente Daten, die außerhalb von React leben, und rendert sie erneut, wann immer sich diese Daten ändern. Du gibst ihm zwei Funktionen: subscribe, die React sagt, wie es auf Änderungen hört, und getSnapshot, die den aktuellen Wert zurückgibt.
Die beiden Display-Komponenten teilen weder Props noch Context, und doch aktualisieren sich beide gemeinsam, weil beide denselben Store abonnieren. Der Button ruft eine einfache Funktion auf, keinen React-Setter. Füge ein drittes <Display name="Sidebar" /> hinzu, und es macht ohne weitere Änderung mit.
Die Syntax
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
subscribe(callback)beginnt zuzuhören, ruftcallbackauf, wann immer sich die Daten geändert haben könnten, und gibt eine Funktion zum Abbestellen zurück. React ruft sie auf, nachdem die Komponente gemountet ist, und ruft die zurückgegebene Funktion beim Unmounten auf.getSnapshot()gibt den aktuellen Wert zurück. React ruft sie bei jedem Render und nach jeder Benachrichtigung auf und vergleicht das Ergebnis mit dem letzten perObject.is. Gleicher Wert, kein Render.getServerSnapshot()(optional) gibt den Wert zurück, der auf dem Server und bei der Hydration verwendet wird.
Definiere subscribe außerhalb der Komponente oder halte sie mit useCallback stabil. Wenn du bei jedem Render eine neue Funktion subscribe übergibst, bestellt React jedes Mal ab und abonniert neu.
Browser-APIs als Stores
Alles, was einen aktuellen Wert hat und ein Event feuert, wenn sich der Wert ändert, passt in diese Form. navigator.onLine mit den Events online und offline ist der klassische Fall.
Schalte dein Netzwerk ab (oder wechsle in den Entwicklerwerkzeugen deines Browsers auf Offline), und der Text ändert sich ohne Neuladen. Das dritte Argument sagt „nimm online an“, wenn auf dem Server gerendert wird, wo es kein navigator gibt. Den Aufruf des Hooks in useOnlineStatus zu packen macht daraus einen Custom Hook, den jede Komponente nutzen kann.
Die Fensterbreite funktioniert genauso:
Füge ein paar Kopien hinzu und ändere die Fenstergröße: Jede Kopie zeigt im selben Moment dieselbe Zahl. Jede Kopie hat ihren eigenen Listener, und jede liest die Breite beim Rendern, also hinkt keine je einen Frame hinterher.
getSnapshot muss einen gecachten Wert zurückgeben
React ruft getSnapshot oft auf und vergleicht Ergebnisse per Referenz. Eine Funktion, die bei jedem Aufruf ein neues Objekt oder Array baut, sieht immer wie eine Änderung aus:
// Broken: a new object on every call
function getSnapshot() {
return { count: store.count, user: store.user };
}
// Also broken: filter returns a new array every time
function getSnapshot() {
return store.todos.filter((t) => !t.done);
}
React rendert, ruft getSnapshot auf, bekommt einen „anderen“ Wert, rendert erneut und so weiter, bis es mit „Maximum update depth exceeded“ stoppt. In der Entwicklung loggt React vorher außerdem „The result of getSnapshot should be cached to avoid an infinite loop“. Ein Produktions-Build wie die Vorschau hier überspringt diese Warnung und meldet nur den finalen Fehler als kurzen Code (Minified React error #185).
Die Lösung ist, die Daten im Store unveränderlich zu halten: Ersetze das Objekt, wenn es sich ändert, und gib die gespeicherte Referenz unverändert zurück.
getSnapshot gibt dasselbe Objekt state zurück, bis add es ersetzt, also rendert die Komponente einmal pro Änderung. Das Filtern passiert in der Komponente, nachdem der Snapshot gelesen wurde, und das ist sicher. Um stattdessen im Store zu filtern, berechne das gefilterte Array, wenn sich die Daten ändern, und speichere es, damit getSnapshot die gespeicherte Kopie zurückgeben kann.
getServerSnapshot und Hydration
Auf dem Server gibt es kein Fenster, kein navigator und kein Abonnement. Das dritte Argument sagt React, was es dort rendern soll:
const width = useSyncExternalStore(
subscribe,
() => window.innerWidth, // in the browser
() => 1024 // on the server, and during hydration
);
React nutzt getServerSnapshot außerdem für den ersten Render im Browser, wenn es Server-HTML hydratisiert, damit beide übereinstimmen. Direkt nach der Hydration liest es getSnapshot und rendert erneut, wenn der echte Wert abweicht. Ohne das dritte Argument wirft der Server-Render „Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering.“ Wenn eine <Suspense>-Grenze über der Komponente steht, schickt der Server den Fallback dieser Grenze, und der Browser rendert stattdessen ihren Inhalt; ohne Grenze schlägt der Server-Render fehl.
useSyncExternalStore vs. useEffect und useState
Du kannst auch mit einem Effekt abonnieren:
function useOnlineStatus() {
const [online, setOnline] = useState(true);
useEffect(() => {
const update = () => setOnline(navigator.onLine);
update();
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}, []);
return online;
}
Das funktioniert, hat aber zwei Schwächen. Der erste Render zeigt immer die anfängliche Vermutung, und der echte Wert kommt einen Render später, nachdem der Effekt gelaufen ist. Und beim Concurrent Rendering (etwa während einer Transition) kann React einen Render mittendrin pausieren; wenn sich der Store während der Pause ändert, können Komponenten, die davor und danach gerendert wurden, verschiedene Werte zeigen. Diese Inkonsistenz heißt Tearing. useSyncExternalStore liest den Wert beim Rendern und lässt React den Render synchron wiederholen, wenn sich der Store geändert hat, also sieht jede Komponente denselben Wert.
Nutze es, wenn die Daten außerhalb von React leben: dein eigenes Store-Modul, eine Browser-API, eine Bibliothek eines Drittanbieters. Die meisten State-Bibliotheken (Redux, Zustand und andere) rufen es in ihren Hooks für dich auf. Für Daten, die deinen Komponenten gehören, bleiben useState, useReducer und Context die richtigen Werkzeuge.
Häufig gestellte Fragen
Wofür wird useSyncExternalStore verwendet?
Um Daten zu lesen, die React nicht gehören und die sich von selbst ändern können: einen außerhalb von React geschriebenen Store, eine State-Bibliothek eines Drittanbieters oder einen Browserwert wie navigator.onLine oder die Fensterbreite. Die Komponente rendert erneut, wann immer der Store React meldet, dass er sich geändert hat.
Was machen subscribe und getSnapshot?
subscribe(callback) beginnt, auf den Store zu hören, ruft bei jeder Änderung callback auf und gibt eine Funktion zurück, die das Zuhören beendet. getSnapshot() gibt den aktuellen Wert zurück. React ruft getSnapshot beim Rendern und nach jeder Benachrichtigung auf und rendert nur erneut, wenn sich der Wert laut Object.is geändert hat.
Warum muss getSnapshot einen gecachten Wert zurückgeben?
React vergleicht das Ergebnis jedes Aufrufs von getSnapshot mit dem vorherigen. Wenn es jedes Mal ein neues Objekt oder Array zurückgibt, sieht React immer eine Änderung, rendert erneut, ruft getSnapshot erneut auf und dreht sich im Kreis, bis es „Maximum update depth exceeded“ wirft. Gib dieselbe Referenz zurück, bis sich die Daten wirklich ändern.
Was ist getServerSnapshot?
Das optionale dritte Argument. Es gibt den Wert zurück, der beim Server-Rendering und bei der Hydration im Browser verwendet wird, damit beide dasselbe HTML erzeugen. Ohne es wirft die Komponente auf dem Server „Missing getServerSnapshot“, und der Inhalt unter der nächsten <Suspense>-Grenze wird stattdessen im Browser gerendert.
Sollte ich useSyncExternalStore oder useEffect mit useState nutzen?
Um externe Daten zu abonnieren, bevorzuge useSyncExternalStore. Es liest den Wert beim Rendern, also ist schon der erste Render korrekt, und jede Komponente sieht denselben Wert, auch beim Concurrent Rendering. useEffect mit useState rendert einmal mit einem veralteten Wert und kann kurzzeitig in verschiedenen Komponenten verschiedene Werte zeigen.