Używaj React Context do tworzenia wspólnego środowiska, a nie jako pudełka na śmieci.
Kontekst rozprowadza wartości poprzez drzewo; nie definiuje własności, nie aktualizuje granic ani modelu stanu. Wolno preferować wąskie dostawcy oraz narzędzia do zarządzania stanem serwera.
Źródła: Most Developers Use React Context for the Wrong Problems
Przekazywanie właściwości nie zawsze oznacza porażkę projektową
React Context często służy jako szybkie rozwiązanie. Wartość przechodzi przez trzy komponenty, które jej w ogóle nie używają. Inny ekran potrzebuje aktualnego użytkownika. Modal musi się otworzyć z głębi struktury drzewa. Przekazywanie właściwości w ten sposób staje się męczące, więc pojawia się provider i struktura nagle wygląda lepiej.
Komponenty pośrednie odrzucają niepotrzebne właściwości. Głębsze komponenty potomne pobierają to, czego potrzebują, za pomocą hooka. Taka struktura wydaje się bardziej przemyślana niż ciągłe przekazywanie tych samych argumentów na każdym poziomie.
To pozorne ulepszenie może zasłonić ważniejsze pytanie: czy ta wartość w ogóle powinna znajdować się w Context?
Kontekst rozwiązuje jeden problem – rodzic może publikować wartości dla potomków bez konieczności przekazywania ich przez każdy pośredni plik. Nie decyduje on o tym, gdzie powinien znajdować się stan, jak często konsumenci powinni go aktualizować, czy wartości niespowiązane powinny dzielić ten sam zbiór, ani który komponent jest właścicielem procesu pracy.
Ty decyzje pozostają bez zmian. Kontekst może je ukryć, ponieważ dostęp do tych danych staje się niewidoczny. Potomek może odczytywać dane bez konieczności ich deklarowania w atrybutach. Dostawca może gromadzić pola niespowiązane ze sobą, ponieważ wszystko wydaje się „globalne”. Mała aktualizacja może dotrzeć do dziesiątek konsumentów, którzy korzystają z tego samego obiektu kontekstu. Widget rzekomo wielokrotnego użycia może polegać na dostawcy, który istnieje tylko na jednej stronie.
Słabością nie jest API – słabością jest używanie łatwiejszego dostępu jako zamiennika dla jaśniejszej struktury własności.
Drążenie właściwościami ma złą reputację, ponieważ powtarzanie się jest widoczne. Wartość zaczyna się wysoko, przechodzi przez elementy układu, aż w końcu dociera do komponentu, który jej potrzebuje. Warstwy pośrednie przypominają instalację hydrauliczną, więc Context wydaje się ulepszeniem.
Czasami to powtarzanie jest przydatną informacją. Pokazuje źródło, na którym gałęzi zależy dana wartość oraz to, jak daleko sięga ta zależność. Właściwości umożliwiają wyszukiwanie danych wejściowych. Gdy wartość się zmienia lub znika, jej wpływ pozostaje lokalny i oczywisty.
Context wymazuje tę ścieżkę. Potomny komponent może zacząć pracować bez aktualizacji pośrednich umów. Drzewo nadal zależy od dostawcy, ale ta relacja nie jest już widoczna na interfejsie komponentu.
To nie oznacza, że każdy długi łańcuch propów musi zostać. Gdy duży poddrzewo rzeczywiście dzieli jedno zadanie, stosowanie łańcuchów staje się zbędnym elementem. Błędem jest traktowanie każdego powtarzającego się propu jako dowodu na brak kontekstu.
Czasami wartość jest zbyt wysoka ze względu na błędne przypisanie własności. Strona przechowuje szkic dialogu i przekazuje go przez kilka warstw do formularza. Kontekst ułatwiłby dostęp, ale lepszy projekt może pozwolić formularzowi lub dialogowi być właścicielem tego szkicu.
Czasami kompozycja eliminuje pośredni etap. Rodzic może stworzyć element potrzebujący danych i przekazać ten element jako children do układu. Układ nigdy nie dowiaduje się o tej zależności.
Powtarzające się propy to objaw. Kontekst jest tylko jednym z rozwiązań i nie stanowi domyślnej diagnozy.
Kontekst nie przekształca bezpiecznie stanu lokalnego w stan globalny
Częstym rozwiązaniem jest tworzenie kontekstu wtedy, gdy kilka komponentów musi aktualizować ten sam stan. Dostawca przechowuje wartości, eksportuje funkcje do ich ustawiania, a każdy potomek może je czytać lub zapisywać.
Często zaczyna się od małych kroków:
const AppContext = createContext(null);
function AppProvider({ children }) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [activeModal, setActiveModal] = useState(null);
const [selectedRecord, setSelectedRecord] = useState(null);
return (
<AppContext.Provider
value={{
sidebarOpen,
setSidebarOpen,
activeModal,
setActiveModal,
selectedRecord,
setSelectedRecord,
}}
>
{children}
</AppContext.Provider>
);
}
Dostęp do danych znajduje się w jednym miejscu, ale znaczenie stanu pozostaje niejasne. Widoczność paska bocznego, otwarte okno modalne oraz wybrany rekord często należą do odrębnych funkcji i są rozwijane w różnym czasie.
Każdy potomek w drzewie może wtedy bezpośrednio przepisać dane przechowywane przez dostawcę. Wiersz w tabeli może ustalić wybór, otworzyć okno modalne lub zamknąć pasek boczny. Komponenty nie wyrażają intencji; po prostu korzystają z wspólnych pól i tworzą taką kombinację, jakiej potrzebują.
Następuje niewidzialne połączenie. Modal zakłada, że wybór już istnieje. Tabela zakłada, że zamknięcie modalu usuwa wybór. Pasek boczny reaguje na proces pracy, którymu nie zarządza. Te zależności nie są uwzględniane w reducerze, operacji ani granicach funkcjonalności — istnieją jedynie dlatego, że wiele komponentów może wywoływać różne funkcje ustawiania.
Przenoszenie pól do Contextu jedynie zmienia miejsce ich przechowywania. Jasność znaczenia nie pojawia się automatycznie.
Mocniejszy dostawca udostępnia operacje zamiast surowych funkcji ustawiania, i to tylko w ramach jednej spójnej kwestii. System dialogowy może oferować openDialog i closeDialog. Dostawca autoryzacji może udostępniać dane sesji oraz operacje logowania. Dostawca tematów może udostępniać aktualny temat oraz specjalną funkcję aktualizacji.
Nie poprzestawaj na stwierdzeniu „kilka komponentów potrzebuje dostępu”. Zastanów się, czy dzielą one jeden cykl życia i jedną kwestię.
Jeden obiekt kontekstowy może stworzyć ogromną powierzchnię aktualizacji
Kontekst również rozprowadza aktualizacje. Gdy zmienia się wartość dostawcy, użytkownicy tego kontekstu mogą ponownie wyrenderować treść. Jest to w porządku, gdy wartość dotyczy jednej kwestii, a użytkownicy rzeczywiście polegają na jej zmianach.
Problemy pojawiają się, gdy jeden dostawca łączy wartości o niepowiązanych częstotliwościach aktualizacji.
function ApplicationProvider({ children }) {
const value = {
user,
theme,
locale,
notifications,
sidebarOpen,
featureFlags,
liveJobStatus,
};
return (
<ApplicationContext.Provider value={value}>
{children}
</ApplicationContext.Provider>
);
}
Przycisk do włączania tematu nie powinien unieważniać tabeli, która potrzebowała tylko wybranego rekordu. Flaga otwarcia paska bocznego nie powinna odświeżać formularza, który wymagał jedynie identyfikatora użytkownika. Połączenie tych pól w jeden obiekt sprawia, że każdy użytkownik reaguje na każdą zmianę, chyba że starannie zapisze dane i podzieli konteksty.
Zespoły czasami zauważają ten koszt dopiero po analizie wydajności. Do tego czasu wiele funkcji już korzysta z takiego rozwiązania. Aby to naprawić, trzeba znaleźć każdego użytkownika i określić, którą część danych faktycznie potrzebował.
Należy preferować wąskich dostawców, którzy są dostosowani do ustalonego tempa aktualizacji. Umieść elementy interfejsu o wysokim wskaźniku zmian w jednym miejscu, a dane sesji stabilne w innym. Zachowuj wartości dostawców w pamięci tymczasowej, gdy tożsamość obiektu inaczej zmieniałaby się przy każdym renderowaniu. Zmierz skutki przed dodawaniem „jeszcze jednego pola” do istniejącej struktury.
Kontekst może ukrywać brakujące granice komponentów
Gdy połączenie określonej funkcji wydaje się trudne, kontekst może zamaskować brakującą granicę. Edytor produktu, który w tle odczytuje informacje o aktywnym produkcie, wersji roboczej, walidacji, uprawnieniach oraz stanie mutacji z kontekstu na poziomie strony, może mieć sens tylko w obrębie tej konkretnej strony.
Część zależności jest naturalnie domyślna, ponieważ opisuje środowisko, a nie dane funkcji – temat, lokalizację, routowanie, wspólny klient HTTP. Takie elementy zazwyczaj mają szeroki zakres i są stosunkowo stabilne.
Dane funkcjonalne są inne. Ukrywanie ich w Context eliminuje niepotrzebny hałas związany z propami, ale jednocześnie zwiększa zależności architektoniczne. Brak propów nie oznacza braku zależności; często oznacza to, że te zależności znalazły się poza zasięgiem wzroku.
Zadaj sobie pytanie, czy komponent może być zrozumiany i przetestowany poza dostawcą, który obecnie go otacza. Jeśli odpowiedź brzmi „nie”, Context może kompensować brak wyraźniejszej umowy dotyczącej propów lub kompozycji.
Context jest często używany zamiast właściwego zarządzania stanem serwera
Kolejnym niewłaściwym zastosowaniem jest wkładanie pobraanych danych do Context, ponieważ kilka komponentów ich potrzebuje. Dostawca żąda danych raz, przechowuje odpowiedź i udostępnia dane, informacje o ładowaniu, błędy oraz możliwość odświeżenia. Wygląda to jak wspólna warstwa danych bez konieczności używania innej biblioteki.
Rzeczywisty stan serwera szybko wymaga dodatkowych rozwiązań: usuwania duplikatów zapytań, reguł dotyczących danych przestarzałych podczas ponownej weryfikacji, prób ponownych, możliwości anulowania operacji, paginacji, aktualizacji po zmianie danych oraz utrzymywania widoczności poprzednich danych podczas odświeżania.
Kontekst nie dostarcza nic z tego. Przekazuje jedynie to, co obliczył dostawca.
Dostawca stopniowo staje się swoistym ręcznie zarządzanym buforem pamięci. Komponenty zależą od jego specyficznych sposobów odświeżania danych. Zmiany w danych są korygowane ręcznie w wspólnych tablicach. Różni dostawcy wprowadzają nieco inne sposoby ładowania danych i formaty komunikatów o błędach. Wyłączenie jednej ścieżki może spowodować utratę danych z bufora; używanie dwóch dostawców może skutkować podwójnym pobieraniem danych.
Istnieją dedykowane biblioteki do zarządzania danymi, ponieważ stan zdalny ma inny cykl życia niż lokalny stan interfejsu użytkownika: właścicielstwo danych na serwerze, aktualność, buforowanie, unieważnianie danych, współdzielenie zasobów oraz różne sposoby radzenia sobie z awariami.
Context może dostarczać skonfigurowanego klienta lub stabilną zależność funkcjonalną. Rzadko jest to odpowiednie miejsce na ponowne budowanie koordynacji zapytań za pomocą zwykłych funkcji useState i useEffect. Skoncentrowany provider, który koordynuje dane serwera z jednym procesem pracy, może nadal być przydatny, ale umożliwienie globalnego dostępu do danych nie rozwiązuje problemu synchronizacji.
Dostosowanie treści przesyłanej nie jest tym samym co zarządzanie danymi zdalnymi.
Context nie zastępuje modelu stanu
Gdy proces pracy staje się złożony, zespoły czasami umieszczają go w Context, aby każdy krok mógł widzieć te same pola. Procesy takie jak weryfikacja konta, onboardowanie, edytory oraz formularze wieloetapowe często kończą się w ten sposób.
To może działać, gdy provider reprezentuje jeden proces pracy i umożliwia kontrolowane przejścia pomiędzy etapami. Staje się to jednak kruche, gdy Context jest jedynie zbiorem pól i funkcji do ich modyfikacji.
Dostawca procesu płatności może zawierać pola adresowe, pola płatności, opcje dostawy, bieżący krok, błędy walidacji, flagi ładowania, flagi modaliwów, sumy oraz wiele funkcji do ustawiania wartości. Każdy element potomny może niezależnie modyfikować dowolną część tych danych.
Dzielenie się pamięcią nie jest tym samym co definiowanie reguł. Zmiana kraju może wymagać anulowania wyboru dostawy, a zmiana dostawy – ponownego obliczenia sum. Powrót o krok wstecz może sprawić, że niektóre pola pozostaną bez zmian, podczas gdy inne staną się nieważne. Niepowodzenie płatności może zmienić przebieg procesu bez usuwania danych adresowych.
Takie relacje wymagają modelu stanu – reduktorów lub wyraźnych operacji, które kodują dopuszczalne przejścia pomiędzy stanami. Kontekst może rozpowszechniać ten model, ale sam nie jest nim.
Bез zasad pojawiają się niemożliwe kombinacje: krok płatności bez ważnej dostawy, zarówno „sukces”, jak i „błąd” jako prawdziwe, opcje dostawy należące do poprzedniej adresy. Nie musisz koniecznie korzystać z formalnej biblioteki maszyn stanowych. Nadal musisz jasno określić, które fakty są niezależne, jakie kombinacje są dopuszczalne oraz które zdarzenia zmieniają kilka pól przy jednej transycji.
Kontekst umożliwia stworzenie dobrego modelu. Nie może on uratować niejasnego modelu jedynie poprzez umieszczenie go na wyższym poziomie w drzewie.
Dostęp globalny może osłabić własność
Wartości w kontekście są łatwe do odczytania i wpisywania z wielu miejsc. Wygoda może podważyć poczucie własności.
Kontekst powiadomień aktualizowany przez obsługę sieci, formularze, strony oraz zadania w tle. Kontekst wyboru modyfikowany przez tabele, okna dialogowe, skróty i zmiany tras. Kontekst preferencji wpisywany bezpośrednio przez każdą kontrolkę pokazującą ustawienie.
Gdy wielu autorów korzysta z tego samego sklepu, odpowiedzialność za poprawność staje się niejasna.
Mocniejsze przypisanie odpowiedzialności wynika z wąsko skoncentrowanych operacji. Wolno preferować metody addNotification, dismissNotification oraz clearExpiredNotifications zamiast surowej metody setNotifications. Wolno również preferować metody signIn, signOut i refreshSession zamiast ogólnej metody setSession. Operacje mają określone znaczenie i umożliwiają dostawcy zachowanie niezmienników.
Nawet w takim przypadku należy utrzymać dostawcę skupionym na swoich zadaniach. Uniwersalny dysponent dla każdej funkcji tworzy globalne powiązania pod inną nazwą.
wiele komponentów może odczytywać aktualnego użytkownika. Znacznie mniej z nich powinno zastępować sesję. Kontekst pozostaje łatwy w utrzymaniu, gdy dostawca kontroluje cały cykl życia, a konsumenty komunikują się poprzez prosty kontrakt.
Dostawca kontekstu może stać się ukrytym monolitem
Komponenty o dużych rozmiarach dają o sobie znać poprzez długość pliku. Dostawcy o dużych rozmiarach pozostają cichsze przez dłuższy czas.
Struktura może wyglądać modułowo – z małymi komponentami i hookami – podczas gdy jeden dostawca kontroluje stan aplikacji, wywołania sieciowe, przechowywanie danych, zdarzenia przeglądarki, uprawnienia, powiadomienia, okna modalne oraz wartości pochodne. Pliki komponentów pozostają krótkie, ponieważ dostawca przejmuje na siebie zadania koordynacji.
To nadal jest monolit; po prostu renderuje niewiele kodu JSX.
Dostawca zmienia się z powodów niezwiązanych ze sobą. Zasady powiadomień, wymagania tematyczne, zmiany sesji oraz procesy obsługi okien modalnych wszystkie edytują ten sam moduł. Każdy użytkownik zależy od jego stabilności. Testy stają się bardziej rozbudowane, ponieważ operacje współpracują ze sobą poprzez wspólny stan. Wydajność pogarsza się, gdy aktualizacje rozprzestrzeniają się. Refaktoryzacja zmusza do rozwiązania problemów wynikających z założeń wbudowanych w wartość używaną wszędzie.
Rozwiązaniem nie jest nieskończone nawijanie się struktur ze względu na to samo. Nawijanie jest do przyjęcia, gdy każdy dostawca ma jasno określoną odpowiedzialność. Kilku skoncentrowanych dostawców zazwyczaj jest bardziej przejrzystych niż jeden obiekt, który udaje, że dzieli wspólny cykl życia.
Znaczenie ma również umiejscowienie. Kontekst potrzebny dla jednej ścieżki powinien obejmować tylko ten poddrzewo, a nie całe aplikacje. Wąskie umiejscowienie ogranicza zarówno zależności, jak i zakres aktualizacji.
Dostępność globalna powinna być osiągana, a nie stanowić domyślnego miejsca przechowywania niewygodnych właściwości.
Kontekst jest doskonały w rozwiązywaniu tych problemów, które faktycznie rozwiązuje
Kontekst sprawdza się wtedy, gdy poddrzewo rzeczywiście dzieli wspólną zależność środowiskową lub stabilny problem do rozwiązania.
Tematy pasują, ponieważ wielu potomków wymaga tej samej opcji prezentacji. Lokalizacja jest możliwa, gdyż formatowanie i tłumaczenie mają szerokie zastosowanie. Wspólny klient API, środowisko routingu, zakres formularza lub pojedynczy proces pracy mogą tam również znajdować się, o ile każdy użytkownik działa w obrębie jednych granic.
Wartość nie musi być statyczna. Powinna mieć spójne znaczenie oraz wzorzec aktualizacji dostosowany do potrzeb użytkowników.
Dobrze zaprojektowany kontekst odpowiada na pytania: w jakim środowisku działa ta poddrzewo, jakie zależności są wspólne dla tych komponentów i do którego procesu pracy należą?
Słabo zaprojektowany kontekst odpowiada na pytania: które wartości sprawiały kłopot przy przekazywaniu jako właściwości? Takie pytanie prowadzi do niepowiązanego stanu, szerokich powierzchni aktualizacji, ukrytych zależności oraz surowych funkcji ustawiania. Struktura systemu znika, a rozróżnienie odpowiedzialności staje się trudniejsze do zrozumienia.
Zanim dodasz kontekst, zapytaj się, czy istnieje znaczący poddrzewo dzielące tę samą wartość, czy elementy konsumujące mają wspólny cykl życia, jak często zmienia się ta wartość oraz czy dostawca może opublikować wąski i stabilny kontrakt. Zapytaj również, jakie problemy miałby ukryć kontekst. Błędne umiejscowienie stanu, brak granic funkcji, słaba kompozycja, cacheowanie stanu serwera lub nieokreślony przepływ pracy nie zostaną rozwiązane dzięki łatwiejszemu dostępowi.
Kontekst powinien rozdzielać decyzje, a nie je unikać
Większość nadużyć nie wynika z błędnego zrozumienia API, lecz z natychmiastowego ulgowania się. Łańcuch właściwości znika, komponenty uzyskują dostęp, a funkcja zostaje wdrożona bez konieczności reorganizacji struktury. Dzisiejszy kod jest łatwiejszy do napisania.
Koszty pojawiają się wtedy, gdy dostęp przewyższa prawo własności. Niepowiązane cykle życia korzystają z tego samego dostawcy. Klienci ponownie renderują treści ze względu na zmiany, których nie zauważają. Komponenty funkcjonalne polegają na ukrytych środowiskach. Dostawcy w tle przekształcają się w prowizoryczne magazyny, bufory żądań oraz silniki przepływu pracy, które nigdy nie otrzymały wyraźnego modelu.
Nazywanie Context słabą biblioteką do zarządzania stanem pomija sedno sprawy. Nigdy nie twierdził on, że definiuje mechanizmy zarządzania stanem; jedynie przenosi wartość wzdłuż drzewa struktury.
Dostępny projekt nadal musi odpowiedzieć na pytania: co reprezentuje ta wartość, który moduł ją posiada, kto ma prawo ją modyfikować, jak często następują aktualizacje oraz które elementy faktycznie do niej należą.
Context jest najskuteczniejszy, gdy te decyzje są już jasne.
Literatura pokrewna
- Context vs Redux Toolkit vs Zustand dla stanu w React — Porównanie Context, Redux Toolkit i Zustand w kontekście portalu wideo, a następnie łączenie sklepów klienckich z TanStack Query do zarządzania stanem serwera.
- Samoadaptacyjne frontendy w JavaScript dla interfejsów napędzanych AI — Jak transmisja strumieniowa, wyszukiwanie wektorów, reaktywny stan, dynamiczne komponenty oraz możliwości obserwacji przekształcają architekturę kliencką w celu stworzenia interfejsów typowo AI.
- Zustand w praktyce: małe sklepy, precyzyjni selektory, aplikacja do śledzenia roślin — Zastąpienie metody prop-drilling oraz skomplikowanych procedur Redux funkcjami create(), selektorami, operacjami asynchronicznymi, middleware persist/devtools, fragmentami danych oraz aplikacją do śledzenia roślin.