Każdy programista React w końcu staje przed tym samym pytaniem: czy sięgnąć po Context, czy może to problem dla Redux? Jeśli budujesz aplikacje od zaledwie kilku miesięcy, szum w sieci sprawia, że brzmi to jak wybór typu „albo-albo”. Niektóre poradniki traktują Redux jako przestarzały balast. Inne ostrzegają, że Context nie skaluje się powyżej prostej listy zadań (to-do list). Żadne z tych skrajnych podejść nie jest pomocne. Prawda jest taka, że te narzędzia rozwiązują inne rodzaje problemów, a mądry wybór zależy od tego, co Twoja aplikacja faktycznie robi.

Problem prop drillingu

Zanim wybierzesz strategię zarządzania stanem, warto zrozumieć problem, który oba te narzędzia starają się wyleczyć. Wyobraź sobie, że budujesz sklep internetowy. Pobierasz profil użytkownika w komponencie App na najwyższym poziomie. W stopce (footer), mały komponent AccountLink potrzebuje tego zdjęcia profilowego. Bez globalnego store'a, obiekt użytkownika musi przejść przez Home, potem Header, potem NavContainer, potem UserDropdown i w końcu trafić do AccountLink. Każda warstwa pomiędzy nimi dotyka danych, których nie używa. To jest właśnie prop drilling.

Prop drilling sprawia, że komponenty stają się kruche. Refaktoryzacja staje się ryzykowna, ponieważ usunięcie jednego pośrednika przerywa cały łańcuch. Wielokrotne użycie (reusability) cierpi, ponieważ komponenty wymagają propsów, które jedynie przekazują dalej w dół. Zarówno Context, jak i Redux eliminują to, pozwalając odległym komponentom subskrybować współdzielone dane bezpośrednio. Jednak sposób, w jaki dostarczają te dane, oraz koszt tego procesu, szybko się od siebie różnią.

Kiedy React Context API jest właściwym wyborem

React Context jest wbudowany w samą bibliotekę. Brak dodatkowych instalacji npm, brak konfiguracji builda, brak plików boilerplate. Tworzysz obiekt kontekstu, opakowujesz część drzewa w Provider i konsumujesz wartość za pomocą useContext w dowolnym zagnieżdżonym komponencie. Dzięki tej prostocie Context świetnie sprawdza się w małych i średnich projektach, gdzie zmiany stanu są rzadkie, a struktura tego stanu jest stosunkowo płaska.

Pomyśl o motywach UI. Użytkownik przełącza się między trybem jasnym a ciemnym być może raz na sesję. Wartość propaguje się do każdego komponentu stylizowanego (styled component), ale zmienia się tak rzadko, że kwestie wydajności niemal nie występują. Status uwierzytelnienia to kolejny klasyczny przypadek. Gdy użytkownik się zaloguje, flaga isAuthenticated i obiekt user pozostają stabilne podczas dziesiątek nawigacji między stronami. Ustawienia języka lub lokalizacji działają w ten sam sposób. Są to szerokie, wolno zmieniające się sygnały, których potrzebuje wiele komponentów, ale które zmienia (mutuje) niewiele z nich.

Haczyk polega na tym, jak Context obsługuje aktualizacje. Gdy wartość Context Providera ulega zmianie, React ponownie renderuje każdy pojedynczy komponent, który konsumuje ten kontekst. W małej aplikacji tego nie poczujesz. W większej aplikacji, jeśli umieścisz szybko zmieniające się dane w szeroko używanym Context, wywołasz kaskadę niepotrzebnych renderów. Możesz podzielić konteksty, aby odizolować zmienność, ale w tym momencie ręcznie projektujesz obejścia optymalizacyjne, które inne narzędzie rozwiązuje automatycznie.

Kiedy Redux Toolkit zasługuje na swoje miejsce

Redux Toolkit został zaprojektowany dla aplikacji, w których stan jest złożony, aktualizacje są częste, a wiele odległych funkcji musi odczytywać i zapisywać te same dane bez konfliktów. Rozważ koszyk zakupowy. Użytkownik dodaje produkt z karty produktu. Ikona koszyka w nagłówku musi zaktualizować licznik. Wysuwa się pasek boczny (sidebar), aby pokazać pozycje w koszyku. Pole wprowadzania kodu rabatowego wykonuje walidację. Strona kasy (checkout) później odczytuje zawartość koszyka. Ten stan jest dotykany przez niezwiązane ze sobą komponenty w całym drzewie i często ulega zmianie.

Redux Toolkit rozwiązuje to poprzez scentralizowany store i jawne wycinki (slices) stanu. Komponenty subskrybują tylko te fragmenty danych, których potrzebują, używając useSelector. Jeśli cena akcji aktualizuje się w panelu sterowania w czasie rzeczywistym, komponent wyświetlający ustawienia profilu użytkownika nie zostanie uruchomiony. Redux pod spodem używa sprawdzania równości referencji, dzięki czemu subskrypcje są granularne. Staje się to krytyczne, gdy liczba komponentów wzrasta do setek.

Redux zapewnia również przewidywalny przepływ danych. Zmiany stanu zachodzą poprzez wysyłane akcje (dispatched actions) obsługiwane przez reducers. Brzmi to jak żargon, ale w praktyce oznacza to, że możesz przeszukać (grep) swój kod w poszukiwaniu addToCart i znaleźć każdą ścieżkę kodu, która modyfikuje koszyk. W dużym zespole ten kontrakt zapobiega błędom. Context, w przeciwieństwie do tego, jest po prostu wartością i funkcją ustawiającą (setter). Każdy konsument może wywołać setState, a śledzenie źródła błędnej wartości oznacza konieczność ustawiania breakpointów w wielu komponentach.

Gdzie naprawdę się różnią

Charakterystyka wydajnościowa to czynnik, który różnicuje te narzędzia bardziej niż cokolwiek innego. Context rozgłasza nową wartość do wszystkich konsumentów bezwarunkowo. Redux powiadamia tylko subskrybentów, których wybrany fragment (slice) uległ zmianie. Jeśli budujesz pulpit nawigacyjny giełdy w czasie rzeczywistym, gdzie notowania odświeżają się co sekundę, Context wymusiłby lawinę globalnych re-renderów. Redux pozwoliłby na przeliczenie tylko komórki kursu i wykresu typu sparkline.

Debugowanie to kolejny obszar, w którym Redux wyprzedza konkurencję w złożonych aplikacjach. Redux DevTools oferuje debugowanie typu time-travel. Możesz cofać się przez każdą wysłaną akcję (dispatched action) i obserwować cofanie stanu. W wieloetapowym procesie zakupowym, obejmującym obliczenia kosztów wysyłki, walidację płatności i odzyskiwanie błędów, możliwość odtworzenia dokładnej sekwencji, która doprowadziła do błędu, jest nieoceniona. Context polega na standardowych narzędziach React DevTools. Możesz sprawdzić aktualne wartości kontekstu, ale nie ma wbudowanego logu akcji ani podglądu różnic w stanie (state diff viewer). Powrót do rozsypywania console.log.

Middleware i efekty uboczne są wpisane w DNA Redux. Redux Toolkit zawiera createAsyncThunk i świetnie integruje się z bibliotekami do pobierania danych. Możesz skoordynować wywołanie API, wyświetlić spinner ładowania, obsłużyć błąd sieci i zapisać wynik w pamięci podręcznej, a wszystko to w ramach przepływu danych Redux. Context nie oferuje wbudowanego wzorca dla logiki asynchronicznej. Albo pobierasz dane wewnątrz komponentów, a następnie przesyłasz wynik do Context, albo opakowujesz dostawców (providers) w autorskie narzędzia asynchroniczne. To działa, ale jest rozwiązaniem doraźnym (ad hoc).

Koszt konfiguracji to obszar, w którym Context zdecydowanie wygrywa. Zbudowanie dostawcy motywu (theme provider) zajmuje około pięciu minut. Redux Toolkit wymaga utworzenia pliku store, zdefiniowania slice'ów i opakowania aplikacji w Providera. Nie jest to już tygodniowa ceremonia, jak w przypadku starego Reduxa i jego gór kodu boilerplate, ale wciąż wymaga więcej konfiguracji niż Context. W przypadku weekendowego projektu pobocznego lub pulpitu z trzema trasami, ten narzut może nie być wart zachodu.

Używanie obu narzędzi w tej samej aplikacji

Nie musisz deklarować lojalności wobec jednej strony. Wiele aplikacji produkcyjnych używa Context do kwestii związanych z globalną powłoką UI, a Redux do danych biznesowych o dużej złożoności domenowej. Powszechnym wzorcem jest trzymanie motywu, lokalizacji i ewentualnie lekkiej flagi uwierzytelnienia w Context, ponieważ każda trasa ich potrzebuje, a zmieniają się one rzadko. W międzyczasie system zarządzania zamówieniami, centrum powiadomień i tabele danych żyją w Redux, gdzie częste aktualizacje i logika międzykomponentowa wymagają precyzyjnej kontroli.

To hybrydowe podejście sprawia, że proste rzeczy pozostają proste, nie wymuszając stosowania pełnego store'a Redux wokół statycznego obiektu motywu. Zapobiega to również zapełnianiu slice'ów Redux elementami interfejsu (UI chrome), które wcale nie wymagały zarządzania stanem na poziomie przemysłowym.

Kluczowy wniosek

Wybór cięższego narzędzia nie jest powodem do dumy. Zacznij od przeanalizowania, jak często zmienia się Twój stan, ile komponentów go dotyka i czy potrzebujesz śledzić mutacje między różnymi zespołami. Jeśli zarządzasz wolno zmieniającymi się, powszechnie współdzielonymi wartościami w aplikacji o umiarkowanej wielkości, Context prawdopodobnie wystarczy. Jeśli Twój stan często ulega zmianie, obejmuje niezwiązane ze sobą funkcje i wymaga jasnej ścieżki audytowej, Redux Toolkit zaoszczędzi Ci problemów.

Wybieraj na podstawie charakteru swojego projektu, a nie na podstawie prelekcji konferencyjnych czy gwiazdek na GitHubie. Koszyk zakupowy z pięćdziesięcioma produktami nie wymaga automatycznie Reduxa, a przełącznik motywu nie potrzebuje globalnego store'a. Dopasuj narzędzie do problemu, a Twój kod pozostanie łatwy w utrzymaniu długo po tym, jak cykl hype'u przeminie.