useSyncExternalStore subskrybuje komponent do danych, które żyją poza Reactem, i renderuje go ponownie, gdy te dane się zmienią. Dajesz mu dwie funkcje: subscribe, która mówi Reactowi, jak nasłuchiwać zmian, i getSnapshot, która zwraca bieżącą wartość.
Dwa komponenty Display nie dzielą propsów ani kontekstu, a jednak aktualizują się razem, bo oba subskrybują ten sam magazyn. Przycisk wywołuje zwykłą funkcję, a nie setter Reacta. Dodaj trzeci <Display name="Sidebar" />, a dołączy bez żadnej innej zmiany.
Składnia
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
subscribe(callback)zaczyna nasłuchiwać, wywołujecallbackzawsze, gdy dane mogły się zmienić, i zwraca funkcję rezygnacji z subskrypcji. React wywołuje ją po zamontowaniu komponentu, a zwróconą funkcję przy odmontowaniu.getSnapshot()zwraca bieżącą wartość. React wywołuje ją przy każdym renderowaniu i po każdym powiadomieniu, a potem porównuje wynik z ostatnim przezObject.is. Ta sama wartość oznacza brak renderowania.getServerSnapshot()(opcjonalna) zwraca wartość do użycia na serwerze i podczas hydracji.
Zdefiniuj subscribe poza komponentem albo utrzymuj ją stabilną przez useCallback. Jeśli przy każdym renderowaniu przekazujesz nową funkcję subscribe, React za każdym razem rezygnuje z subskrypcji i subskrybuje ponownie.
API przeglądarki jako magazyny
Wszystko, co ma bieżącą wartość i uruchamia zdarzenie, gdy wartość się zmienia, pasuje do tego kształtu. Klasycznym przypadkiem jest navigator.onLine ze zdarzeniami online i offline.
Wyłącz sieć (albo przełącz na Offline w narzędziach deweloperskich przeglądarki), a tekst zmieni się bez przeładowania. Trzeci argument mówi „zakładaj, że jest online" przy renderowaniu na serwerze, gdzie nie ma navigator. Owinięcie wywołania hooka w useOnlineStatus robi z niego własny hook, którego może użyć każdy komponent.
Szerokość okna działa tak samo:
Dodaj kilka kopii i zmień rozmiar okna: każda kopia pokazuje tę samą liczbę w tej samej chwili. Każda kopia ma własne nasłuchiwanie i każda odczytuje szerokość podczas renderowania, więc żadna nigdy nie jest o klatkę w tyle.
getSnapshot musi zwracać zapamiętaną wartość
React często wywołuje getSnapshot i porównuje wyniki po referencji. Funkcja, która przy każdym wywołaniu buduje nowy obiekt albo tablicę, zawsze wygląda jak zmiana:
// 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 renderuje, wywołuje getSnapshot, dostaje „inną" wartość, renderuje ponownie i tak dalej, aż zatrzyma się z "Maximum update depth exceeded". W trybie deweloperskim React najpierw wypisuje też "The result of getSnapshot should be cached to avoid an infinite loop". Build produkcyjny, taki jak tutejszy podgląd, pomija to ostrzeżenie i zgłasza tylko końcowy błąd jako krótki kod (Minified React error #185).
Rozwiązaniem jest utrzymywanie danych w magazynie jako niemutowalnych: zastępuj obiekt, gdy się zmienia, i zwracaj zapisaną referencję bez zmian.
getSnapshot zwraca ten sam obiekt state, dopóki add go nie zastąpi, więc komponent renderuje się raz na każdą zmianę. Filtrowanie odbywa się w komponencie, po odczytaniu migawki, co jest bezpieczne. Żeby zamiast tego filtrować w magazynie, wyliczaj przefiltrowaną tablicę przy zmianie danych i ją zapisuj, żeby getSnapshot mogła zwracać zapisaną kopię.
getServerSnapshot i hydracja
Na serwerze nie ma okna, navigator ani subskrypcji. Trzeci argument mówi Reactowi, co tam wyrenderować:
const width = useSyncExternalStore(
subscribe,
() => window.innerWidth, // in the browser
() => 1024 // on the server, and during hydration
);
React używa getServerSnapshot także przy pierwszym renderowaniu w przeglądarce, gdy hydratuje HTML z serwera, żeby oba się zgadzały. Zaraz po hydracji odczytuje getSnapshot i jeśli prawdziwa wartość jest inna, renderuje ponownie z nią. Bez trzeciego argumentu renderowanie na serwerze rzuca "Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering." Jeśli nad komponentem jest granica <Suspense>, serwer wysyła jej widok zastępczy, a przeglądarka renderuje zamiast tego jej zawartość; bez granicy renderowanie na serwerze się nie udaje.
useSyncExternalStore a useEffect i useState
Możesz subskrybować przez efekt:
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;
}
To działa, ale ma dwie słabości. Pierwsze renderowanie zawsze pokazuje początkowe przypuszczenie, a prawdziwa wartość przychodzi jedno renderowanie później, po uruchomieniu efektu. Do tego przy renderowaniu współbieżnym (na przykład podczas przejścia) React może wstrzymać renderowanie w połowie; jeśli w trakcie przerwy magazyn się zmieni, komponenty wyrenderowane przed nią i po niej mogą pokazywać różne wartości. Ta niespójność nazywa się tearing. useSyncExternalStore odczytuje wartość podczas renderowania i sprawia, że React synchronicznie powtarza renderowanie, jeśli magazyn się zmienił, więc każdy komponent widzi tę samą wartość.
Używaj go, gdy dane żyją poza Reactem: twój własny moduł magazynu, API przeglądarki, zewnętrzna biblioteka. Większość bibliotek stanu (Redux, Zustand i inne) wywołuje go za ciebie w swoich hookach. Dla danych, które należą do twoich komponentów, właściwymi narzędziami pozostają useState, useReducer i kontekst.
Najczęściej zadawane pytania
Do czego służy useSyncExternalStore?
Do odczytu danych, których React nie jest właścicielem i które mogą zmieniać się same: magazynu napisanego poza Reactem, zewnętrznej biblioteki stanu albo wartości przeglądarki, takiej jak navigator.onLine czy szerokość okna. Komponent renderuje się ponownie zawsze, gdy magazyn powie Reactowi, że coś się zmieniło.
Co robią subscribe i getSnapshot?
subscribe(callback) zaczyna nasłuchiwać magazynu, wywołuje callback przy każdej zmianie i zwraca funkcję, która kończy nasłuchiwanie. getSnapshot() zwraca bieżącą wartość. React wywołuje getSnapshot podczas renderowania i po każdym powiadomieniu, a renderuje ponownie tylko wtedy, gdy wartość zmieniła się według Object.is.
Dlaczego getSnapshot musi zwracać zapamiętaną wartość?
React porównuje wynik każdego wywołania getSnapshot z poprzednim. Jeśli za każdym razem zwraca nowy obiekt albo tablicę, React zawsze widzi zmianę, renderuje ponownie, znowu wywołuje getSnapshot i zapętla się, aż rzuci "Maximum update depth exceeded". Zwracaj tę samą referencję, dopóki dane naprawdę się nie zmienią.
Co to jest getServerSnapshot?
Opcjonalny trzeci argument. Zwraca wartość do użycia podczas renderowania na serwerze i podczas hydracji w przeglądarce, żeby obie strony dały ten sam HTML. Bez niego komponent rzuca na serwerze "Missing getServerSnapshot", a zawartość pod najbliższą granicą <Suspense> jest zamiast tego renderowana w przeglądarce.
Czy używać useSyncExternalStore, czy useEffect z useState?
Do subskrypcji danych z zewnątrz wybieraj useSyncExternalStore. Odczytuje wartość podczas renderowania, więc już pierwsze renderowanie jest poprawne, a każdy komponent widzi tę samą wartość nawet przy renderowaniu współbieżnym. useEffect z useState renderuje się raz z nieaktualną wartością i może przez chwilę pokazywać różne wartości w różnych komponentach.