Zasady korzystania z hooków

Hooki są nowym dodatkiem w Reakcie 16.8. Pozwalają one używać stanu i innych funkcjonalności Reacta, bez użycia klas.

Hooki są javascriptowymi funkcjami, ale podczas korzystania z nich musisz pamiętać o dwóch ważnych zasadach. Stworzyliśmy wtyczkę do lintera, która automatycznie wymusza stosowanie tych zasad:

Wywołuj hooki tylko z najwyższego poziomu kodu

Nie wywołuj hooków wewnątrz pętli, instrukcji warunkowych czy zagnieżdżonych funkcji. Korzystaj z hooków tylko z najwyższego poziomu kodu twoich komponentów funkcyjnych. Przestrzegając tej zasady, zyskujesz pewność, że hooki zostaną wywołane w tej samej kolejności, za każdym razem gdy komponent jest renderowany. To właśnie pozwala Reactowi na właściwe przechowywanie stanu pomiędzy kolejnymi wywołaniami useState i useEffect (Jeśli ciekawi cię ten temat, dogłębnie wyjaśnimy go w kolejnym podrozdziale.)

Wywołuj hooki tylko w komponentach funkcyjnych

Nie wywołuj hooków wewnątrz zwykłych javascriptowych funkcji. Zamiast tego możesz:

  • ✅ Wywoływać hooki wewnątrz reactowych komponentów funkcyjnych.
  • ✅ Wywoływać hooki wewnątrz własnych hooków (więcej na ten temat dowiemy się w następnym rozdziale).

Przestrzegając tej zasady, upewniasz się, że cała logika związana ze stanem komponentu jest wyraźnie widoczna w jego kodzie źródłowym.

Wtyczka dla ESLinta

Wydaliśmy wtyczkę dla ESLinta o nazwie eslint-plugin-react-hooks, która wymusza stosowanie tych dwóch zasad. Jeśli chcesz ją wypróbować, możesz dodać ją do swojego projektu w następujący sposób:

npm install eslint-plugin-react-hooks --save-dev
// Twoja konfiguracja ESLint
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // Sprawdza stosowanie zasad hooków
    "react-hooks/exhaustive-deps": "warn" // Sprawdza zależności efektów
  }
}

W przyszłości zamierzamy dołączyć te wtyczkę do Create React App i podobnych narzędzi.

Możesz teraz przejść do następnego rozdziału, gdzie wyjaśniamy, jak pisać własne hooki. W tym rozdziale postaramy się uzasadnić, dlaczego narzucamy takie zasady.

Wyjaśnienie

Jak dowiedzieliśmy się wcześniej, w ramach pojedynczego komponentu możemy używać wielu hooków stanu i efektów:

function Form() {
  // 1. Użyj zmiennej stanu do przechowania imienia
  const [name, setName] = useState('Mary');

  // 2. Użyj efektu, aby zapisać dane formularza
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Użyj zmiennej stanu do przechowania nazwiska
  const [surname, setSurname] = useState('Poppins');

  // 4. Użyj efektu, aby zaktualizować tytuł strony
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

Skąd zatem React wie, jaką wartość stanu zwrócić przy kolejnych wywołaniach funkcji useState? Tajemnica tkwi w tym, że React polega na kolejności, w jakiej hooki są wywoływane. Nasz przykład zadziała, ponieważ kolejność wywoływania hooków jest taka sama przy każdym renderowaniu:

// ------------
// Pierwsze renderowanie
// ------------
useState('Mary')           // 1. Zainicjalizuj zmienną stanu imienia wartością „Mary”
useEffect(persistForm)     // 2. Dodaj efekt odpowiedzialny za przechowywanie danych formularza
useState('Poppins')        // 3. Zainicjalizuj zmienną stanu nazwiska wartością „Poppins”
useEffect(updateTitle)     // 4. Dodaj efekt odpowiedzialny za aktualizację tytułu

// -------------
// Drugie renderowanie
// -------------
useState('Mary')           // 1. Odczytaj zmienną stanu przechowującą imię (argument został zignorowany)
useEffect(persistForm)     // 2. Zastąp efekt odpowiedzialny za przechowywanie danych
useState('Poppins')        // 3. Odczytaj zmienną stanu przechowującą nazwisko (argument został zignorowany)
useEffect(updateTitle)     // 4. Zastąp efekt odpowiedzialny za aktualizację tytułu

// ...

Tak długo, jak kolejność wywoływania hooków pozostaje taka sama pomiędzy kolejnymi renderowaniami, React może powiązać lokalny stan z każdym z nich. A co wydarzy się, jeśli umieścimy wywołanie hooka (na przykład efektu persistForm) wewnątrz instrukcji warunkowej?

  // 🔴 Łamiemy pierwszą zasadę, używając hooka wewnątrz instrukcji warunkowej
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }

Warunek name !== '' jest spełniony przy pierwszym renderze, więc uruchamiany jest ten hook. Jednakże przy kolejnym renderze użytkownik może wyczyścić wartości formularza, powodując, że warunek nie będzie spełniony. Teraz, w związku z tym, że pominęliśmy hook podczas renderowania, kolejność wywoływania hooków zostaje zachwiana:

useState('Mary')           // 1.Odczytaj zmienną stanu przechowującą imię (argument został zignorowany)
// useEffect(persistForm)  // 🔴 Ten hook został pominięty!
useState('Poppins')        // 🔴 2 (a był 3). Nie uda się odczytać zmiennej stanu
useEffect(updateTitle)     // 🔴 3 (a był 4). Nie uda się zastąpić efektu

React nie wiedziałby, co zwrócić dla drugiego wywołania hooka useState. React spodziewał się, że drugie wywołanie hooka w tym komponencie będzie odpowiadało wywołaniu efektu persistForm, tak jak podczas poprzedniego renderowania. Nie jest to już jednak prawdą. Od tej chwili każde kolejne wywołanie hooka, po tym, jak jeden został pominięty, również przesunęłoby się o jeden, prowadząc do błędów.

Dlatego właśnie hooki muszą być wywoływane z najwyższego poziomu kodu komponentów. Jeśli chcesz, żeby efekt działał pod jakimś warunkiem, możesz umieścić ten warunek wewnątrz hooka:

  useEffect(function persistForm() {
    // 👍 Tym razem nie łamiemy pierwszej zasady
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

Zauważ, że nie musisz o tym pamiętać, jeśli użyjesz dostarczonej przez nas reguły lintera. Teraz jednak wiesz także, dlaczego hooki działają w ten sposób i jakim problemom zapobiega stosowanie tej reguły.

Kolejne kroki

W końcu jesteśmy gotowi na to, aby nauczyć się pisać własne hooki! Własne hooki pozwalają łączyć hooki dostarczone przez Reacta we własne abstrakcje i współdzielić logikę związaną ze stanem pomiędzy komponentami.