Każdy programista Reacta w końcu trafia na tę samą ścianę. Pobierasz obiekt użytkownika w swoim głównym komponencie App. Następnie przekazujesz go w dół. I w dół. Przez wrapper trasy, przez szkielet układu, przez kontener paska bocznego, tylko po to, aby maleńki komponent awatara znajdujący się trzy warstwy głębiej mógł wyświetlić zdjęcie profilowe. Komponenty znajdujące się pośrodku nie dbają o ten obiekt użytkownika. One jedynie przekazują przesyłkę dalej. To jest właśnie prop drilling, który zamienia czyste drzewo komponentów w frustrującą zabawę w głuchy telefon.

Prawdziwy ból zaczyna się, gdy zmienia się struktura tych danych. Być może backend zaczyna zagnieżdżać dane jako user.profile.avatar zamiast user.avatar. Nagle musisz edytować interfejsy TypeScript lub PropTypes w pięciu plikach, które same w sobie nigdy nie używają tych danych. Właśnie tutaj wkracza React Context API.

Jak Context zmienia przepływ danych

Wyobraź sobie Context jako router WiFi umieszczony w centrum Twojego domu. Bez niego musiałbyś ciągnąć kable Ethernet przez każdy pokój, aby uzyskać sygnał na laptopa. Dzięki niemu router nadaje sygnał w powietrzu, a każde urządzenie z odpowiednim hasłem może połączyć się bezpośrednio. Ściany nie mają znaczenia.

W terminologii React, korzeń Twojej aplikacji może nadawać dane w całym drzewie komponentów, nie prosząc każdej warstwy o pełnienie roli kuriera. Każdy zagnieżdżony komponent może zasubskrybować ten sygnał i otrzymać dokładnie to, czego potrzebuje.

Trzy kluczowe elementy

Context API sprowadza się do trzech ruchomych części.

React.createContext() ustawia kanał nadawczy. Zwraca obiekt zawierający Providera i (w starszym kodzie) Consumera. Musisz wywołać to tylko raz dla danej funkcjonalności.

Provider to komponent, który opakowuje sekcję Twojego drzewa. Przyjmuje on jedną właściwość (prop) o nazwie value. Wszystko, co umieścisz w tej właściwości, staje się dostępne dla każdego potomka, bez względu na to, jak głęboko się znajduje.

useContext to Hook, który pozwala komponentowi funkcyjnemu podłączyć się do tego nadawania. Wewnątrz swojego komponentu przekazujesz obiekt kontekstu, który utworzyłeś, do useContext, a on zwraca aktualną wartość. To wszystko. Żadnych wrapperów, żadnych dodatkowych propsów.

Zanim pojawiły się Hooki, trzeba było używać wzorca Consumer z render props. Działało to, ale tworzyło mnóstwo wcięć i niepotrzebnych wrapperów. useContext spłaszczył to wszystko do pojedynczej linii wewnątrz ciała funkcji.

Kiedy Context ma sens

Nie sięgaj po Context z przyzwyczajenia. Został on stworzony dla danych, które wiele niezwiązanych ze sobą komponentów współdzieli w różnych gałęziach drzewa. Dobrymi kandydatami są:

  • Ustawienia motywu. Nie tylko tryb jasny lub ciemny, ale także tokeny odstępów (spacing tokens), palety kolorów i skale czcionek. Ręczne przekazywanie ich przez każdy stylizowany przycisk i modal szybko staje się uciążliwe.
  • Uwierzytelnianie użytkownika. Status zalogowania, tablica uprawnień lub aktualny obiekt użytkownika. Twój pasek nagłówka, widget pulpitu nawigacyjnego i strażnik prywatnych tras mogą znajdować się w różnych zakątkach drzewa.
  • Preferencje językowe. Stringi lokalizacji, formaty dat i symbole walut. Komponenty liściaste, takie jak etykiety formularzy, potrzebują ich bez konieczności informowania o tym każdego rodzica na drodze.
  • Dane koszyka zakupowego. Liczba produktów, wartość całkowita i funkcje dodawania do koszyka. Odznaka w nagłówku i strona kasy potrzebują tego samego stanu, ale zazwyczaj znajdują się w zupełnie innych gałęziach układu.

Praktyczny przełącznik motywów

Jednym z najprostszych sposobów na zobaczenie Contextu w akcji jest przełącznik motywu. Oto jak możesz go skonfigurować, nie pomijając szczegółów, które mają znaczenie.

Po pierwsze, utwórz plik ThemeContext.js. Wywołaj React.createContext() i zapisz wynik. Następnie zbuduj komponent ThemeProvider, który zarządza aktualnym motywem za pomocą useState lub useReducer. Opakuj dzieci (children) w Providera swojego kontekstu, przekazując obiekt zawierający zarówno aktualny motyw, jak i funkcję do jego przełączania. Wyeksportuj zarówno ThemeProvider, jak i sam obiekt kontekstu.

Po drugie, przejdź do punktu wejścia aplikacji. Zaimportuj ThemeProvider i opakuj nim całą swoją aplikację. Jeśli pominiesz ten krok, wszystko, co będzie próbowało później odczytać kontekst, zobaczy jedynie wartość domyślną.

Po trzecie, wewnątrz komponentu Header lub Content zaimportuj obiekt kontekstu oraz useContext. Wywołaj Hook, rozpakuj (destructure) motyw oraz funkcję przełączania i warunkowo zastosuj klasy CSS. Dodaj przycisk, który wywołuje przełączanie. Komponent nigdy nie otrzymuje właściwości theme od swojego rodzica. Pobiera sygnał bezpośrednio z powietrza.

Prop Drilling, Context czy Redux?

Wybór między tymi narzędziami to nie tyle kwestia lojalności, co kształtu Twojego stanu.

Prop drilling jest jak najbardziej w porządku przy dwóch lub trzech poziomach głębokości. Jest jawny, łatwy do prześledzenia w IDE i sprawia, że zależności są oczywiste. Problemy pojawiają się dopiero wtedy, gdy zaczynasz przekazywać ten sam prop przez sześć lub siedem warstw.

Context API jest dostarczane wraz z samym Reactem. Oznacza to brak dodatkowego rozmiaru paczki (bundle size) i brak zewnętrznej konfiguracji. Świetnie radzi sobie ze stanem globalnym o małej lub średniej wielkości, zwłaszcza z danymi, które zmieniają się rzadko, takimi jak motywy czy profile użytkowników.

Redux wymaga zainstalowania dodatkowych bibliotek i napisania kodu boilerplate. Opłaca się jednak, gdy logika stanu jest złożona, gdy wiele fragmentów (slices) stanu wchodzi ze sobą w głębokie interakcje lub gdy potrzebujesz debugowania typu time-travel i middleware. W przypadku prostych danych globalnych Redux to overkill.

Rzeczywistość wydajnościowa, o której nikt nie mówi

Oto haczyk, który odróżnia implementacje juniorów od tych tworzonych przez seniorów. Gdy wartość Context Providera ulega zmianie, każdy komponent konsumujący ten kontekst przechodzi re-render. Nie ma znaczenia, czy konkretny fragment stanu, na którym zależy danemu komponentowi, pozostał bez zmian. React widzi nową referencję i planuje aktualizację.

Jeśli wrzucisz cały stan aplikacji do jednego gigantycznego StoreContext, w praktyce skleisz ze sobą całe UI. Zmiana ustawień motywu spowoduje re-render koszyka zakupowego, wykresów w panelu sterowania i listy powiadomień. To niepotrzebna praca.

Rozdziel konteksty według domen. Zachowaj ThemeContext dla ustawień wizualnych, UserContext dla danych profilowych i CartContext dla stanu handlowego. Jeśli użytkownik zmieni swoją nazwę wyświetlaną, nagłówek zaktualizuje się bez wpływu na siatkę produktów. Pamiętaj też, aby uważać na to, co przekazujesz do właściwości value Providera. Jeśli przekazujesz literale obiektów { theme, toggleTheme } bezpośrednio podczas renderowania, tworzysz nową referencję przy każdym renderowaniu, co wywołuje niepotrzebne aktualizacje. Ustabilizuj tę strukturę za pomocą useMemo, jeśli wartość zawiera funkcje lub dane nie-primitywne.

Błędy, które marnują godziny

Dwa błędy regularnie dopadają zespoły.

Zapomnienie o wyeksportowaniu obiektu kontekstu. Łatwo jest wyeksportować komponent ThemeProvider, a następnie próbować wywołać useContext(ThemeProvider). To nie tak działa. Hook potrzebuje obiektu kontekstu zwróconego przez createContext, a nie komponentu opakowującego. Jeśli wyeksportujesz tylko Providera, Twoi konsumenci nie będą mieli czego zaimportować.

Wywołanie useContext poza jego Providerem. Hook zwraca domyślną wartość przekazaną do createContext. Jeśli nie przekazałeś wartości domyślnej, otrzymasz undefined. Jeśli drzewo komponentów renderuje konsumenta wyżej w DOM niż Providera, lub jeśli Providera brakuje całkowicie, Twoje dane po prostu nie dotrą. Sprawdź dwukrotnie, czy Twój plik index lub root faktycznie opakowuje aplikację.

Najważniejszy wniosek

React Context nie jest rewolucją w zarządzaniu stanem. To celowe narzędzie do konkretnego problemu przestrzennego: dostarczania danych do odległych komponentów bez zamieniania każdej warstwy w urządzenie pocztowe. Używaj go do danych naprawdę globalnych, rozdzielaj konteksty według domen, aby chronić wydajność renderowania, i zawsze opakowuj drzewo odpowiednim Providerem, zanim spróbujesz odczytać sygnał. Opanuj te nawyki, a Twoje drzewa komponentów pozostaną czyste, szybkie i łatwe do zrozumienia.