useSyncExternalStore (React 18+) ist die offizielle React-API, um auf externe Datenquellen zu reagieren — Stores aus Libraries wie Redux/Zustand/Valtio, Browser-APIs wie localStorage/window.matchMedia, oder eigene Event-Emitter. Vor 18 nutzten Libraries useEffect + useState, was unter Concurrent-Rendering zu Tearing führen konnte (verschiedene Teile der UI zeigen unterschiedliche Versionen des Stores). useSyncExternalStore synchronisiert garantiert konsistent. Für App-Code direkt selten gebraucht — der Hook ist primär für Library-Authoren. Wer Redux nutzt: useSelector ist intern auf useSyncExternalStore aufgebaut.
Signatur
const snapshot = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot?
);- subscribe: Funktion, die einen Callback registriert. React ruft sie auf, übergibt einen
notify-Callback. Wenn der Store sich ändert, Store ruftnotify(). Rückgabe der subscribe-Funktion: Unsubscribe-Cleanup. - getSnapshot: liefert den aktuellen Wert aus dem Store. MUSS bei gleichen Daten dieselbe Referenz liefern.
- getServerSnapshot (optional): wie getSnapshot, aber für SSR.
Beispiel — localStorage als Store
import { useSyncExternalStore } from 'react';
function subscribe(callback) {
window.addEventListener('storage', callback);
return () => window.removeEventListener('storage', callback);
}
export function useLocalStorageValue(key) {
return useSyncExternalStore(
subscribe,
() => localStorage.getItem(key),
() => null // beim SSR kein localStorage
);
}
// Verwendung
function ThemeDisplay() {
const theme = useLocalStorageValue('theme');
return <p>Theme: {theme ?? 'default'}</p>;
}Wenn localStorage.setItem('theme', 'dark') aus einer anderen Tab-Instanz aufgerufen wird, feuert das storage-Event und React rendert die Komponente mit dem neuen Wert.
Beispiel — Online-Status
import { useSyncExternalStore } from 'react';
function subscribeOnline(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
export function useOnline() {
return useSyncExternalStore(
subscribeOnline,
() => navigator.onLine,
() => true // SSR-Fallback
);
}
function NetworkStatus() {
const online = useOnline();
return <p>Status: {online ? '🟢 online' : '🔴 offline'}</p>;
}Beispiel — Eigener Event-Store
// Store-Modul außerhalb von React
let counter = 0;
const listeners = new Set();
export const counterStore = {
getSnapshot() { return counter; },
subscribe(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
},
increment() {
counter++;
listeners.forEach(fn => fn());
},
};
// In React
function Counter() {
const value = useSyncExternalStore(
counterStore.subscribe,
counterStore.getSnapshot
);
return (
<>
<p>Count: {value}</p>
<button onClick={counterStore.increment}>+1</button>
</>
);
}Der Store lebt außerhalb von React — beliebige Stellen können counterStore.increment() aufrufen, alle subscribten Komponenten rendern neu.
getSnapshot-Falle: gleiche Daten → gleiche Referenz
React vergleicht den Rückgabewert per Object.is. Wenn getSnapshot() bei unveränderten Daten ein neues Object liefert, denkt React „neu" und rendert unnötig — im Worst-Case Infinite-Loop.
// FALSCH: bei jedem Aufruf neues Object
const todos = useSyncExternalStore(
store.subscribe,
() => ({ todos: store.todos, count: store.todos.length })
// → React sieht „neu" bei jedem Render → Infinite-Loop möglich
);
// KORREKT: Snapshot ist eine STABILE Referenz im Store
const todos = useSyncExternalStore(
store.subscribe,
() => store.todos // gleicher Array, solange unverändert
);Best Practice: getSnapshot ist eine PURE Funktion ohne neue Object-Allokation. Wenn Aggregation nötig: useMemo außen rum, oder die Aggregation im Store cachen.
Wann useSyncExternalStore?
Sinnvoll wenn:
- Library-Autor für State-Management (Redux, Zustand, Valtio).
- Integration mit Browser-APIs, die Events feuern (online/offline, mediaQuery, intersection).
- Eigener globaler Event-Bus für App-übergreifenden State.
Nicht sinnvoll:
- Normaler Komponenten-State →
useState. - Server-State → TanStack Query, SWR.
- Tief verschachtelte Komponenten-Kommunikation → Context.
Besonderheiten
useSyncExternalStore ist primär ein Library-Hook.
In App-Code direkt selten benutzt. Wenn du Redux oder Zustand nutzt: deren Hooks (useSelector, Zustand-Hook) sind intern auf useSyncExternalStore aufgebaut. Direkter Einsatz nur für Custom-Stores oder Browser-API-Wrapper.
subscribe ist die Anmelde-Funktion, NICHT der Listener selbst.
React übergibt einen callback an deine subscribe-Funktion. Du registrierst diesen Callback beim externen System. Wenn das System „Update!" sagt, rufst du callback() auf. NICHT umgekehrt.
getSnapshot MUSS Reference-stable sein bei gleichen Daten.
Der häufigste Bug-Vektor. () => ({a: x, b: y}) liefert jedes Mal neues Object → React sieht „neu" → Endlos-Loop oder unnötige Renders. Lösung: gleicher Wert → gleiche Object-Referenz.
getServerSnapshot für SSR — sonst Hydration-Mismatch.
Bei Server-Rendering läuft getSnapshot nicht (kein Browser-Store). Der dritte Parameter liefert den Server-Wert. Ohne diesen Parameter wirft useSyncExternalStore im SSR.
Concurrent-Sicher — kein Tearing.
Pre-18-Pattern (useEffect + useState) konnte unter Concurrent-Rendering inkonsistente Snapshots in verschiedenen Komponenten zeigen. useSyncExternalStore garantiert: alle Konsumenten sehen denselben Snapshot pro Render-Pass.
subscribe sollte STABIL sein — nicht bei jedem Render neu.
Wenn subscribe bei jedem Render eine neue Funktion ist, re-subscribt React jedes Mal. Daher: subscribe als Modul-Level-Funktion oder mit useCallback stabilisieren.
getSnapshot ist synchron — kein `await`, kein `fetch`.
Snapshot muss SOFORT verfügbar sein. Async-Quellen: erst in den Store laden, dann getSnapshot liest den geladenen Wert. Für Server-State: TanStack Query, das eigene Caching-Lösungen hat.
useSyncExternalStore ist die offizielle Lösung für 'Selectors'.
Redux useSelector nutzt es intern. Custom-Selectors mit Memoization (z.B. useMemo auf getSnapshot-Resultat) sind möglich, aber die Library-Hooks haben das oft bereits optimiert.
Weiterführende Ressourcen
Externe Quellen
- useSyncExternalStore – react.dev
- Tearing in Concurrent Rendering – React 18 RFC
- Zustand Documentation
- Redux useSelector – React Redux