Strona główna / Artykuły

Czy ten hook zasługuje na swoje miejsce? Dziesięć pytań do recenzji kodu React

Przyswoj dziesięć konkretnych pytań do recenzji, które pomagają wykryć zbędne stany, przestarzałe efekty, pętle renderowania, niepotrzebną memoizację oraz zbyt duże komponenty w kodzie React Hook.

Wydawca: REACTAPP.TOP6 min czytania1324 słów

Źródła: Hooks

Większość błędów w React Hook nie wygląda jak błędy w pull request. Brakująca zależność, efekt kopiujący props do stanu lub użycie useCallback wokół obrońcy, którego nikt nie memoizuje, przechodzą przez proces sprawdzania, a później objawiają się jako przestarzałe ekraniki, dodatkowe renderowania lub nagromadzone słuchacze. Poniżej dziesięć częstych błędów z Hook zostało przekształconych w pytania do sprawdzenia, które można zadać dowolnemu komponentowi, wraz z mechanizmem działania każdego z nich oraz prostszą formą, jakiej zazwyczaj wymaga kod.

Pytania dotyczące stanu

Czy tę wartość można obliczyć podczas renderowania?

Częsty wzorzec polega na przechowywaniu fullName w stanie i używaniu efektu, aby wywołać setFullName(`${firstName} ${lastName}`) za każdym razem, gdy zmienia się któryś z imion. Komponent renderuje się raz z starym wartością, efekt uruchamia się po narysowaniu, a funkcja ustawiająca planuje drugi render. Przez jedną kadru interfejs jest niespójny, a do utrzymania dodaje się kolejną elementarną część.

Lepiej obliczyć to w ten sposób: const fullName = `${firstName} ${lastName}`;. React ponownie uruchamia komponent przy każdym renderze, więc wartość zawsze jest zsynchronizowana z jego danymi wejściowymi. Jeśli coś można wywnioskować z propów lub stanu, to nie jest to stan. Zobacz także dlaczego synchronizacja stanu z useEffect ma negatywne skutki.

Czy komponent przechowuje więcej stanu, niż potrzebuje?

Ta sama pomyłka w większym wymiarze: formularz z trzema wywołaniami useState dla firstName, lastName i fullName. Każda zmiana imienia musi teraz zostać odzwierciedlona w fullName, a każda droga, która o tym zapomni, powoduje błąd. Zachowaj dwa niezależne pola wprowadzania danych i wyprowadź trzecie na ich podstawie. Dla każdego useState zastanów się, czy wartość można odbudować na podstawie pozostałych; jeśli tak, usuń to pole.

Czy zmiana tej wartości wymaga aktualizacji ekranu?

Stan istnieje po to, aby kierować procesem renderowania. Identyfikator timera, węzeł DOM, poprzednia wartość właściwości lub instancja zewnętrzna (mapa, wykres, klient socket) zazwyczaj nie wpływa na to, co jest rysowane, więc użycie useState(null) w takich przypadkach powoduje bezsensowne renderowanie przy każdej przypisaniu. Zamiast tego użyj const timerRef = useRef(null);: zapisanie timerRef.current zachowuje się między renderowaniami bez konieczności planowania nowego renderowania. Z drugiej strony nigdy nie powinieneś odczytywać refu, aby zdecydować, co wyświetlić. Jeśli interfejs musi odzwierciedlać daną wartość, to jest to przecież stan.

Pytania dotyczące efektów

Czy tablica zależności zawiera wszystko, co efekt odczytuje?

Podanie [], aby efekt „wykonał się raz”, jak w przykładzie useEffect(() => { fetchUser(userId); }, []);, zamraża wartość userId od pierwszego renderowania. Gdy zmienia się prop, nic nie jest ponownie pobierane, więc interfejs pokazuje niewłaściwego użytkownika. Użycie [userId] rozwiązuje ten problem.

Tablica nie służy do regulacji częstotliwości wykonywania efektu; określa jedynie, jakie wartości są przez niego odczytywane. Jeśli efekt wykonywa się zbyt często, należy zmienić to, co jest odczytywane (umieścić funkcję wewnątrz efektu, polegać na wartości prymitywnej zamiast obiektu) zamiast ukrywać zależności. Reguła lintingu react-hooks/exhaustive-deps wykrywa niemal wszystkie takie przypadki, dlatego należy traktować jej ostrzeżenia jak błędy. Efekty typu fetch powinny również ignorować lub anulować przestarzałe żądania podczas czyszczenia, gdy userId zmienia się szybko.

Czy ten efekt może sam siebie uruchomić?

useEffect(() => { setCount(count + 1); }, [count]); to pętla: aktualizacja zmienia count, co powoduje ponowne uruchomienie efektu w nieskończoność. Funkcjonalny aktualizator setCount(prev => prev + 1) odczytuje najnowszą wartość bez konieczności przechowywania count w zamykaniu funkcji, dzięki czemu zależność może zostać usunięta, a efekt uruchamia się tylko przy pierwszym załadowaniu. Należy zauważyć, że i tak wartość wzrasta o jedno przy każdym załadowaniu, a w trybie Strict Mode aplikacja jest ładowana dwa razy podczas rozwoju, więc lokalnie może się wydawać, że wartość skacze o dwa.

Lepiejszym pytaniem jest często to, czy aktualizacja w ogóle powinna znajdować się w efekcie. Reakcje na działania użytkownika trafiają do obsługi zdarzeń; wartości pochodne są obliczane podczas renderowania. Efekty służą do synchronizacji z systemami spoza React.

Czy efekt cofa to, co zostało ustawione?

Efekt, który wywołuje jedynie window.addEventListener('resize', handleResize), dodaje nowego słuchacza przy każdym uruchomieniu i nigdy go nie usuwa. Należy zwrócić funkcję do czyszczenia, która wywołuje window.removeEventListener z tym samym obsługownikiem. React uruchamia ją przed ponownym wywołaniem efektu oraz przy jego dezaktywacji. Bez niej występują wycieki pamięci, wielokrotne uruchamianie się obsługowników oraz aktualizacje komponentów, które już nie istnieją.

Szybka weryfikacja: każde wywołanie add, connect lub setInterval w efekcie wymaga odpowiadającego im wywołania remove, disconnect lub clearInterval w jego wartości zwracanej. sprawdzenie dziesięciu najczęstszych błędów w useEffect omawia więcej sposobów awarii efektów.

Pytania dotyczące memoizacji

Czy ta obliczanie jest rzeczywiście kosztowne?

Otoczenie łączenia ciągów znaków funkcją useMemo z [user] jako zależnością kosztuje więcej, niż oszczędza. React nadal alokuje funkcję zwrotną i tablicę oraz porównuje zależności przy każdym renderowaniu, co wymaga więcej pracy niż po prostu połączenie dwóch ciągów znaków. Kod staje się trudniejszy do odczytania, a jeśli user to nowy obiekt przy każdym renderowaniu, memo i tak zostanie ponownie obliczone.

Należy używać zwykłych wyrażeń. Zastosuj useMemo tylko wtedy, gdy chodzi o wyraźnie kosztowne operacje, takie jak sortowanie lub filtrowanie dużej listy, albo gdy zmemorizowane dziecko lub inny Hook wymaga stabilnego odniesienia do obiektu. Najpierw sprawdź to za pomocą Profilera w React DevTools.

Czy coś zależy od tożsamości tej funkcji?

useCallback zapewnia stabilność odniesienia do funkcji, co ma znaczenie tylko wtedy, gdy coś porównuje takie odniesienia: dziecko otoczone funkcją React.memo lub lista zależności innego Hooka. Otaczanie obsługi kliknięcia, która jedynie zapisuje informacje do logu i jest przypisana zwykłemu elementowi button, nie przynosi żadnych korzyści, ponieważ DOM nie przejmuje się tym, czy funkcja jest nowa. Napisz zwykłą funkcję i dodaj useCallback, gdy możesz określić komponent, który jej potrzebuje. Kompilator React może automatycznie zastosować taką mechanizm memozacji; sprawdź aktualną dokumentację, aby dowiedzieć się o jego dostępności w twojej konfiguracji.

Pytania dotyczące struktury

Czy każdy Hook jest wywoływany bezwarunkowo, w tej samej kolejności?

Deklarowanie useState tylko wewnątrz if (isLoggedIn) narusza Zasady Hooków. React dopasowuje Hooki na podstawie pozycji wywołania w liście dla danego komponentu, a nie na podstawie nazwy. Jeśli pominie się jedno wywołanie, każdy kolejny Hook odczytuje dane z sąsiedniego pola, co powoduje błędne wartości lub błąd w czasie wykonywania związany ze zmienioną liczbą Hooków.

Należy wywołać wszystkie Hooki na najwyższym poziomie, a następnie rozgałęzić kod: najpierw zadeklarować stan user, a jeśli użytkownik nie jest zalogowany, zwrócić null. To samo dotyczy pętli i wcześniejszych zwracania wartości. Więcej informacji na temat mechanizmów znajdziesz w artykule dlaczego istnieją Zasady Hooków.

Czy ten komponent wykonuje zbyt wiele zadań?

Komponent, który łączy setki linijek logiki pobierania danych, stanu i biznesowej z markupiem, jest trudny do odczytania, testowania i modyfikacji. Wyodrębnij tę logikę do specjalnego Hooka. Hook useUserData(userId) zarządza stanem user oraz efektem, który wywołuje fetchUser(userId).then(setUser), gdy zmieni się userId, a następnie zwraca user. UserProfile sprowadza się do const user = useUserData(userId); wraz z wyświetlaniem user?.name.

Dzięki temu uzyskujesz:

  • Możliwość ponownego użycia logiki bez jej kopiowania między komponentami.
  • Testy logiki niezależne od markupu.
  • Krótsze komponenty, które łatwo jest przejrzeć.
  • Jedno miejsce do modyfikacji logiki pobierania lub cacheowania.

Pamiętaj, że każdy użytkownik specjalnego Hooka ma swój własny stan: dzieli się logiką, a nie danymi.

Główne wnioski

  • Wartości pochodne to wyrażenia, a nie stan ani efekty.
  • Zachowuj stan w minimalnym stopniu; wartości, które nigdy nie trafiają na ekran, umieszczaj w refach.
  • Mассивy zależności opisują, co odczytuje dany efekt, więc sprawdzaj je dokładnie.
  • Każdy efekt, który coś konfiguruje, musi to później zniszczyć.
  • Memoizuj tylko wtedy, gdy wymaga tego pomiar lub konsument wrażliwy na referencje.
  • Zawołuj Hooki bezwarunkowo i przenieś duże bloki logiki do niestandardowych Hooków.

Większość tych problemów wynika z używania Hooka z przyzwyczajenia. Zadanie pytania „Czy naprawdę potrzebujemy tego Hooka tutaj?” podczas przeglądu jest tanie i pozwala uniknąć długich sesji debugowania.

Literatura pokrewna

  • Lista sprawdzania kodu w React: stan pochodny, reduktory, efekty i memo — Dowiedz się, jak rozpoznawać siedem powszechnych problemów podczas przeglądania kodu w React, od duplikowanego stanu i ręcznego pobierania danych po niewłaściwe użycie efektów i nadmierne memoizowanie, oraz co napisać zamiast tego.