React 18 wprowadził useSyncExternalStore, hook, który pozwala komponentom odczytywać dane z dowolnego źródła znajdującego się poza drzewem Reacta — API przeglądarki, WebSocketów czy zewnętrznych magazynów danych (stores) — bez błędów wizualnych, które towarzyszyły staremu wzorcowi kopiowania tych danych za pomocą useState i useEffect. Jeśli kiedykolwiek widziałeś migotanie interfejsu użytkownika (UI), ponieważ jedna jego część pokazywała stary rozmiar okna, podczas gdy inna już odzwierciedlała nowy, ten hook jest rozwiązaniem, na które czekałeś.

Dlaczego stan zewnętrzny wymaga nowego hooka

Komponenty Reacta od zawsze mogły subskrybować rzeczy istniejące poza ich własnym zakresem: obiekt window, magazyn Redux czy połączenie WebSocket. Konwencjonalnym podejściem było ustawienie subskrypcji wewnątrz useEffect, przesyłanie przychodzących wartości do lokalnego stanu za pomocą setState i pozwolenie Reactowi na ponowne wyrenderowanie. Działa to dobrze, gdy React renderuje synchronicznie, ale React 18 wprowadził renderowanie współbieżne (concurrent rendering), w którym komponent może być renderowany wielokrotnie, zanim przeglądarka faktycznie dokona malowania (paint). W tym trybie wzorzec „kopiowania do lokalnego stanu” może powodować tearing — sytuację, w której różne części UI odczytują różne snapshoty tej samej zewnętrznej wartości podczas jednego cyklu renderowania.

useSyncExternalStore został stworzony, aby zapobiegać zjawisku tearingu. Prosi on źródło zewnętrzne o snapshot (aktualną wartość) oraz funkcję subscribe (sposób powiadamiania o zmianach). React wywołuje subscribe podczas montowania komponentu i automatycznie odsubskrybuje go przy odmontowywaniu. Za każdym razem, gdy snapshot zwrócony przez getSnapshot ulegnie zmianie, React planuje renderowanie, które odczytuje tę samą migawkę dla całego drzewa komponentów, co gwarantuje spójność.

Dwie wymagane funkcje

Funkcja Co robi
subscribe Przyjmuje callback, który musi zostać wywołany za każdym razem, gdy zewnętrzna wartość ulegnie zmianie. Zwraca funkcję unsubscribe, którą React wywoła podczas czyszczenia (cleanup).
getSnapshot Zwraca aktualną wartość ze źródła zewnętrznego. Musi być ona stabilna referencyjnie — czyli musi to być ten sam obiekt funkcji przy każdym renderowaniu — aby React mógł poprawnie porównać snapshoty.

Minimalna implementacja sprawdzająca status online przeglądarki wygląda następująco:

function useOnlineStatus() {
  return useSyncExternalStore(
    (onChange) => {
      window.addEventListener('online', onChange);
      window.addEventListener('offline', onChange);
      return () => {
        window.removeEventListener('online', onChange);
        window.removeEventListener('offline', onChange);
      };
    },
    () => navigator.onLine
  );
}

Gdy jakikolwiek komponent wywoła useOnlineStatus(), React wyrenderuje go ponownie tylko wtedy, gdy navigator.onLine faktycznie zmieni wartość, a każde renderowanie będzie widzieć tę samą wartość.

Zasady, które zapobiegają problemom z hookiem

  1. Zwracaj najmniejszą niezbędną część danych.
    Jeśli zewnętrzny magazyn przechowuje duży obiekt, a komponent interesuje tylko jedno pole, zwróć tylko to pole. Zwracanie większych struktur powoduje niepotrzebne renderowania.

  2. Memozuj getSnapshot.
    Jeśli będziesz tworzyć tę funkcję na nowo przy każdym renderowaniu, React potraktuje ją jako nowe źródło, co może prowadzić do nieskończonej pętli. Użyj useCallback lub zdefiniuj ją poza komponentem.

  3. Unikaj zwracania nowych obiektów przy każdym wywołaniu.
    Zwracanie nowego obiektu (np. { count: store.getCount() }) tworzy nową referencję przy każdym renderowaniu, co sprawia, że React myśli, iż snapshot się zmienił, i uruchamia nieskończoną pętlę renderowania. Używaj prymitywów, ciągów znaków, liczb lub zmemozowanych obiektów.

  4. Nie używaj go do wewnętrznego stanu komponentu.
    useState wciąż jest właściwym narzędziem dla danych, które żyją tylko wewnątrz komponentu. useSyncExternalStore wprowadza narzut, który nie jest potrzebny przy stanie lokalnym.

Kiedy po niego sięgać

  • API przeglądarki – rozmiar okna, media queries, navigator.onLine, status baterii.
  • Magazyny niezależne od frameworka – Zustand, Redux, MobX lub dowolny niestandardowy magazyn udostępniający API subskrypcji.
  • Mutowalne źródła aktualizowane poza Reactem – wiadomości WebSocket, zdarzenia zmian w IndexedDB, powiadomienia Service Worker.

Jeśli Twoje źródło danych znajduje się już wewnątrz Reacta (np. stan komponentu nadrzędnego), pozostań przy useState lub context.

Co mówi społeczność

Pierwsi użytkownicy donoszą, że useSyncExternalStore eliminuje migotanie, które obserwowali podczas zmiany rozmiaru okien w konfiguracji z renderowaniem współbieżnym. Niektóre biblioteki już przeszły na to API w swoich wewnętrznych hookach, obiecując bardziej przewidywalne zachowanie w różnych wersjach Reacta. Kosztem jest nieco większy wysiłek intelektualny: programiści muszą pamiętać o stabilności obu funkcji i unikać zwracania nowych obiektów przy każdym wywołaniu.

Co dalej

Nadchodzące wersje Reacta mogą ściślej określić zasady korzystania z useSyncExternalStore, być może dodając wbudowane pomocniki dla popularnych API przeglądarek. Śledź oficjalny blog Reacta, aby sprawdzić, czy stary wzorzec naśladujący useEffect nie zostanie wycofany. Tymczasem hook jest stabilny i stanowi część publicznego API, więc możesz bezpiecznie refaktoryzować istniejący kod obsługujący stan zewnętrzny.

Szybki test

Interaktywne demo prezentujące synchronizację rozmiaru okna, wykrywanie statusu online/offline oraz prosty store Zustand znajduje się pod adresem https://usesyncexternalstore.vercel.app/. Kod źródłowy, wraz z komentarzami, jest dostępny na https://github.com/dev48v/usesyncexternalstore.

Podsumowanie: useSyncExternalStore zapewnia Reactowi niezawodny pomost do wszelkich mutowalnych danych znajdujących się poza jego cyklem renderowania, zapobiegając zjawisku tearingu i niepotrzebnym renderowaniom. Stosuj go do źródeł zewnętrznych, dbaj o to, aby snapshot był minimalny i stabilny, a ciężką pracę związaną z zachowaniem spójności zostaw Reactowi.