Otwórz niemal dowolny kod źródłowy Reacta, a dostrzeżesz ten sam odruch. Programista musi śledzić wartość, więc sięga po useState. Potrzebuje licznika? useState. Tymczasowa wartość inputa? useState. Boolean do przełączania modala? useState. Wkrótce pojedynczy komponent zawiera tuzin oddzielnych hooków, z których każdy zarządza skrawkiem danych, które mogą, ale nie muszą być zachowywane pomiędzy renderami. Rezultatem jest przeładowany kod, dodatkowe re-rendery i stan rozproszony niczym drobne monety po całym komponencie.
Ten nawyk jest zrozumiały. useState to pierwszy hook, którego większość z nas się uczy, i on po prostu działa. Ale to, że coś działa, nie oznacza, że jest odpowiednio dopasowane. Traktowanie każdego fragmentu danych jako stanu reaktywnego tworzy problemy, które ujawniają się dopiero wtedy, gdy komponent zaczyna rosnąć.
To, że coś się zmienia, nie oznacza, że potrzebuje stanu
Nie każda zmienna, która zmienia się w czasie, powinna znajdować się w useState. Niektóre wartości są jedynie wynikiem czegoś innego, co już posiadasz. Jeśli przechowujesz pełne imię i nazwisko użytkownika w stanie tylko dlatego, że łączysz firstName i lastName, masz teraz dwa źródła prawdy. Gdy firstName zostanie zaktualizowane w wyniku re-renderu rodzica, Twój stan fullName pozostanie nieaktualny, dopóki nie uruchomisz kolejnego efektu, aby go zsynchronizować. Nie potrzebujesz efektu synchronizującego. Potrzebujesz wartości pochodnej.
const fullName = `${firstName} ${lastName}`;
Obliczaj ją podczas renderowania. Jeśli wyliczanie jest kosztowne, zmemoizuj ją. Ale nie nadawaj jej własnego hooka useState, chyba że użytkownik może edytować to pełne imię niezależnie od jego części.
Ta sama zasada dotyczy filtrowanych list. Jeśli przechowujesz zarówno allItems, jak i filteredItems w stanie, podwajasz obszar wymagany do utrzymania kodu. Filtruj podczas renderowania. Przechowuj tablicę źródłową i tekst filtra w stanie, a następnie wylicz widoczną listę. Gwarantuje to, że filtrowana lista nigdy nie będzie niespójna ze źródłem.
Niektóre wartości nigdy nie powinny wyzwalać re-renderów
useState istnieje konkretnie po to, aby poinformować React, że coś się zmieniło i DOM może wymagać aktualizacji. Jeśli wartość się zmienia, ale żadna część interfejsu użytkownika nie przejmuje się tą zmianą, lepszym narzędziem jest useRef.
Timery i interwały to klasyczny przykład. Przechowywanie identyfikatorów setInterval w stanie powoduje re-render za każdym razem, gdy uruchamiasz lub zatrzymujesz timer, mimo że użytkownik nie widzi ID interwału. Ref przechowuje tę wartość bez powiadamiania Reacta. Ta sama logika dotyczy śledzenia poprzednich propsów, mierzenia węzłów DOM przed malowaniem (paint) czy przechowywania ostatniego callbacku dla niestandardowego hooka. Zadaj sobie pytanie: czy ta wartość musi pojawić się na ekranie? Jeśli odpowiedź brzmi „nie”, prawdopodobnie nie potrzebuje useState.
Same węzły DOM również powinny znajdować się w refach. Choć możesz przechowywać element DOM w stanie, zrobienie tego wyzwala re-render po wykonaniu callbacku refa. W większości przypadków potrzebujesz węzła jedynie do metody imperatywnej lub pomiaru, a nie po to, by renderować go w inny sposób.
Pułapka Boole'a
Powiązane kwestie interfejsu użytkownika mają tendencję do rozrastania się, gdy każda flaga otrzymuje własny hook. Widzisz komponenty z isLoading, isError i isSuccess zdefiniowanymi jako trzy oddzielne wartości boolean. Problem polega na tym, że te trzy stany nie są niezależne. Jeśli isLoading i isSuccess są jednocześnie prawdziwe, Twój interfejs znajduje się w niemożliwym stanie, a mimo to TypeScript i React pozwolą Ci go wyrenderować.
Grupowanie powiązanych stanów zapobiega tym nieprawidłowym kombinacjom. Zamiast trzech wartości boolean, śledź pojedynczy string statusu: 'idle', 'loading', 'success' lub 'error'. Tylko jeden może być aktywny w danym czasie, co eliminuje niemożliwe stany na poziomie typów. Jeśli dane są bardziej złożone, obiekt z discriminated union jeszcze bardziej porządkuje sprawę. Gdy złapiesz się na aktualizowaniu kilku wywołań useState wewnątrz tego samego obsługiwacza zdarzeń (event handler), jest to sygnał, że te wartości powinny tworzyć całość.
Sięgnij po useReducer, a nie kolejny useState
Przychodzi taki moment, w którym aktualizacje stanu stają się grą w uderzanie w dziury. Wywołujesz setA, potem setB, a następnie warunkowo setC, a wszystko to wewnątrz jednej funkcji. Kolejny programista, który przeczyta ten kod, musi prześledzić całą sekwencję, aby zrozumieć, co komponent właściwie robi.
useReducer świetnie się tu sprawdza. Nie zastępuje on useState dlatego, że jest bardziej zaawansowany; zastępuje go, ponieważ logika tego wymaga. Reducer centralizuje sposób zmiany stanu. Zamiast rozrzucać instrukcje imperatywne po obsługach zdarzeń, wysyłasz intencję: dispatch({ type: 'submitted' }). Reducer decyduje, jak ma wyglądać kolejny stan. Dzięki temu testowanie staje się trywialne, ponieważ logika stanu jest czystą funkcją. Ułatwia to także debugowanie, ponieważ każda zmiana pozostawia możliwą do prześledzenia akcję.
Nie potrzebujesz Reduxa, aby uzasadnić stosowanie reducera. Jeśli masz trzy lub więcej zmiennych stanu, które aktualizują się jednocześnie, lub jeśli Twój kolejny stan silnie zależy od poprzedniego, reducer drastycznie upraszcza komponent.
Gdzie faktycznie znajduje się stan
Czasami problemem nie jest to, jak przechowujesz stan, ale gdzie. Częstym błędem jest podnoszenie stanu do komponentu nadrzędnego tylko dlatego, że może on być potrzebny gdzie indziej. Jeśli tylko jeden komponent końcowy korzysta z danej części stanu, pozostaw ją tam. To jest kolokacja, która zmniejsza promień rażenia wprowadzanych zmian. Nie zmuszaj komponentu nadrzędnego do ponownego renderowania tylko dlatego, że komponent potomny został otwarty
