Czyste komponenty

Niektóre funkcje javascriptowe są czyste. Funkcje czyste wykonują tylko obliczenia i nic więcej. Stosując się ściśle do pisania komponentów jako funkcji czystych, można uniknąć całej grupy dezorientujących błędów i nieprzewidywalnego zachowania w miarę jak kod rozwija się. Aby uzyskać te korzyści, musisz jednak przestrzegać kilku zasad.

W tej sekcji dowiesz się

  • Czym jest czystość i w jaki sposób pomaga uniknąć błędów
  • Jak tworzyć czyste komponenty przez trzymanie zmian poza fazą renderowania
  • Jak używać trybu rygorystycznego (ang. Strict Mode) do znajdowania błędów w komponentach

Czystość: Komponenty jako formuły

W informatyce (zwłaszcza w świecie programowania funkcyjnego), funkcja czysta ma następujące cechy:

  • Dba o swoje własne sprawy. Nie zmienia żadnych obiektów ani zmiennych, które istniały przed jej wywołaniem.
  • Takie same dane wejściowe, taki sam wynik. Dla takich samych danych wejściowych funkcja czysta powinna zawsze zwracać ten sam wynik.

Być może znasz już jeden przykład funkcji czystych: formuły matematyczne.

Rozważ taki wzór: y = 2x.

Jeśli x = 2, to wtedy y = 4. Zawsze.

Jeśli x = 3, to wtedy y = 6. Zawsze.

Jeśli x = 3, to y nie będzie czasami wynosić 9 albo –1, albo 2.5 zależnie od pory dnia czy notowań na giełdzie.

Jeśli y = 2x oraz x = 3, to y zawsze będzie wynosić 6.

Jeśli zamienilibyśmy to na funkcję javascriptową, wyglądałaby ona tak:

function double(number) {
return 2 * number;
}

W powyższym przykładzie double jest funkcją czystą. Jeśli przekażesz jej 3, zawsze zwróci 6.

React jest zaprojektowany wokół tego konceptu. React zakłada, że każdy komponent, który piszesz, jest funkcją czystą. Oznacza to, że komponenty reactowe, które piszesz, zawsze muszą zwracać ten sam JSX dla tych samych danych wejściowych:

function Recipe({ drinkers }) {
  return (
    <ol>    
      <li>Zagotuj {drinkers} filiżanki wody.</li>
      <li>Dodaj {drinkers} łyżki herbaty i {0.5 * drinkers} łyżkę/łyżki przypraw.</li>
      <li>Dodaj {0.5 * drinkers} filiżankę/filiżanki mleka i cukier dla smaku.</li>
    </ol>
  );
}

export default function App() {
  return (
    <section>
      <h1>Przepis na Herbatę Chai</h1>
      <h2>Dla dwóch osób</h2>
      <Recipe drinkers={2} />
      <h2>Dla większej grupy</h2>
      <Recipe drinkers={4} />
    </section>
  );
}

Kiedy przekażesz drinkers={2} do Recipe, zawsze zwróci on JSX zawierający 2 filiżanki wody.

Jeśli przekażesz drinkers={4}, zawsze zwróci on JSX zawierający 4 filiżanki wody.

Dokładnie tak jak formuła matematyczna.

Możesz myśleć o swoich komponentach jak o przepisach kuchennych: jeśli będziesz stosować się do nich i nie wprowadzisz nowych składników podczas procesu gotowania, otrzymasz ten sam posiłek za każdym razem. To “danie” to JSX, który komponent dostarcza do Reacta na potrzeby renderowania.

Przepis na herbatę dla x osób: weź x filiżanek wody, dodaj x łyżek herbaty i 0.5x łyżek przypraw oraz 0.5x filiżanek mleka

Autor ilustracji Rachel Lee Nabors

Skutki uboczne: (nie)zamierzone konsekwencje

Proces renderowania w Reakcie zawsze musi być czysty. Komponenty powinny jedynie zwracać swój JSX i nie zmieniać żadnych obiektów ani zmiennych, które istniały przed renderowaniem – to sprawiałoby, że komponenty nie są czyste!

Oto komponent, który łamie tę zasadę:

let guest = 0;

function Cup() {
  // Źle: zmiana istniejącej zmiennej!
  guest = guest + 1;
  return <h2>Filiżanka herbaty dla gościa #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

Ten komponent odczytuje i nadpisuje zmienną guest zadeklarowaną poza nim. Oznacza to, że wywołanie tego komponentu wielokrotnie spowoduje wygenerowanie różnego JSX! Co więcej, jeśli inne komponenty odczytują guest, również wygenerują różny JSX, w zależności od momentu renderowania! To nie jest przewidywalne.

Wróćmy do naszej formuły y = 2x. Teraz nawet jeśli x = 2, nie możemy być pewni, że y = 4. Nasze testy mogłyby zakończyć się niepowodzeniem, nasi użytkownicy byliby zdumieni, a samoloty mogłyby spaść z nieba – widzisz, jak mogłoby to prowadzić do niezrozumiałych błędów!

Możesz naprawić ten komponent, przekazując guest jako właściwość:

function Cup({ guest }) {
  return <h2>Filiżanka herbaty dla gościa #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}

Teraz twój komponent jest czysty, ponieważ JSX, który zwraca, zależy tylko od właściwości guest.

Ogólnie, nie powinno się oczekiwać, że komponenty zostaną wyrenderowane w określonej kolejności. Nie ma znaczenia, czy zostanie wywołane y = 2x przed czy po y = 5x: obie formuły będą rozwiązywane niezależnie od siebie. W ten sam sposób każdy komponent powinien “myśleć samodzielnie” i nie próbować koordynować się ani zależeć od innych podczas renderowania. Renderowanie przypomina egzamin szkolny: każdy komponent powinien obliczać JSX samodzielnie!

Dla dociekliwych

Wykrywanie nieczystych obliczeń za pomocą trybu rygorystycznego

Choć być może jeszcze nie wszystkie zostały przez ciebie użyte, w Reakcie istnieją trzy rodzaje danych wejściowych, które można odczytywać podczas renderowania: właściwości, stan i kontekst. Zawsze powinno się traktować te dane wejściowe tylko jako do odczytu.

Kiedy chce się zmienić coś w odpowiedzi na interakcję użytkownika, powinno się ustawić stan zamiast zapisywać do zmiennej. Nigdy nie powinno się zmieniać istniejących zmiennych lub obiektów podczas renderowania komponentu.

React oferuje “tryb rygorystyczny” (ang. Strict Mode), w którym, w trybie deweloperskim, wywołuje on funkcję każdego komponentu dwukrotnie. Poprzez dwukrotne wywołanie, tryb rygorystyczny pomaga znaleźć komponenty, które łamią te zasady.

Zauważ, że oryginalny przykład wyświetlał “Gość #2”, “Gość #4” i “Gość #6” zamiast “Gość #1”, “Gość #2” i “Gość #3”. Oryginalna funkcja była nieczysta, więc jej dwukrotne wywołanie zepsuło ją. Ale naprawiona, czysta wersja działa nawet wtedy, gdy funkcja jest wywoływana dwukrotnie za każdym razem. Czyste funkcje tylko obliczają, więc ich dwukrotne wywołanie nic nie zmienia — tak samo jak dwukrotne wywołanie double(2) nie zmienia tego, co jest zwracane, a rozwiązanie równania y = 2x dwukrotnie nie zmienia wartości y. Te same dane wejściowe, te same dane wyjściowe. Zawsze.

Tryb rygorystyczny nie ma wpływu na wersję produkcyjną, więc nie spowolni aplikacji dla użytkowników. Aby wybrać tryb rygorystyczny, musisz opakować swój główny komponent w <React.StrictMode>. Niektóre frameworki robią to domyślnie.

Lokalna mutacja: mały sekret twojego komponentu

W powyższym przykładzie problemem było to, że komponent zmieniał istniejącą wcześniej zmienną podczas renderowania. Często nazywa się to “mutacją”, aby brzmiało trochę bardziej przerażająco. Funkcje czyste nie mutują zmiennych i obiektów spoza ich zakresu, które zostały utworzone przed wywołaniem funkcji - co czyniłoby je nieczystymi!

Jednak jest całkowicie dopuszczalne zmienianie zmiennych i obiektów, które właśnie utworzono podczas renderowania. W tym przykładzie stworzoną tablicę [], przypisuje się do zmiennej cups, a następnie używa funkcji push, aby dodać do niej tuzin filiżanek:

function Cup({ guest }) {
  return <h2>Filiżanka herbaty dla gościa #{guest}</h2>;
}

export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

Jeśli zmienna cups lub tablica [] zostałyby utworzone poza funkcją TeaGathering, byłby to ogromny problem! Zmieniałoby się istniejący wcześniej obiekt, dodając do niego elementy.

Jednakże tu jest to całkowicie w porządku, ponieważ zostały one utworzone w trakcie tego samego renderowania, wewnątrz funkcji TeaGathering. Żaden kod spoza TeaGathering nigdy nie będzie wiedział, że to się wydarzyło. Nazywa się to “lokalną mutacją” — to jak taki mały sekret twojego komponentu.

Gdzie można uruchamiać efekty uboczne

Podczas gdy programowanie funkcyjne opiera się głównie na czystości, w pewnym momencie, gdzieś, coś musi się zmienić. To jest właśnie cel programowania! Zmiany te — aktualizacja ekranu, uruchomienie animacji, zmiana danych — nazywane są efektami ubocznymi. To rzeczy, które dzieją się “na boku”, a nie podczas renderowania.

W Reakcie efekty uboczne zazwyczaj należą do procedur obsługi zdarzeń. Są to funkcje, które React uruchamia, gdy wykonasz jakąś akcję - na przykład gdy klikasz przycisk. Chociaż procedury obsługi zdarzeń są zdefiniowane wewnątrz twojego komponentu, nie uruchamiają się one podczas renderowania! Dlatego nie muszą być one czyste.

Jeśli wyczerpano wszystkie inne opcje i nie można znaleźć odpowiedniej procedury obsługi zdarzeń dla twojego efektu ubocznego, nadal można dołączyć go do zwróconego JSXa za pomocą wywołania useEffect w twoim komponencie. Powiadamia to Reacta, aby wykonał go później, po renderowaniu, gdy efekty uboczne są dozwolone. Jednakże ten sposób powinien być rozwiązaniem ostatecznym.

Kiedy to możliwe, staraj się wyrazić swoją logikę tylko za pomocą renderowania. Zdziwisz się, jak daleko możesz zajść używając tego sposobu!

Dla dociekliwych

Dlaczego React dba o czystość?

Pisanie czystych funkcji wymaga pewnych nawyków i dyscypliny. Ale otwiera także fantastyczne możliwości:

  • Twoje komponenty mogą działać w różnym środowisku — na przykład na serwerze! Ponieważ zwracają one ten sam wynik dla tych samych danych wejściowych, jeden komponent może obsłużyć wiele żądań użytkowników.
  • Możesz poprawić wydajność, pomijając renderowanie komponentów, których dane wejściowe się nie zmieniły. Jest to bezpieczne, ponieważ czyste funkcje zawsze zwracają te same wyniki, więc mogą być bezpiecznie przechowywane w pamięci podręcznej.
  • Jeśli niektóre dane zmieniają się w trakcie renderowania głębokiego drzewa komponentów, React może zrestartować renderowanie bez marnowania czasu na zakończenie poprzedniego, przestarzałego renderowania. Czystość sprawia, że ​​można bezpiecznie zatrzymać obliczenia w dowolnym momencie.

Każda nowa funkcjonalność Reacta, którą budujemy, opiera się na tej z czystości. Od pobierania danych, przez animacje, aż po wydajność, zachowanie komponentów w czystości uwalnia moc paradygmatu Reacta.

Powtórka

  • Komponent musi być czysty, co oznacza:
    • Dba o swoje sprawy. Nie powinien zmieniać żadnych obiektów ani zmiennych, które istniały przed renderowaniem.
    • Takie same dane wejściowe, taki sam wynik. Dla tych samych danych wejściowych komponent powinien zawsze zwracać ten sam JSX.
  • Renderowanie może wystąpić w dowolnym momencie, dlatego komponenty nie powinny zależeć od kolejności renderowania się nawzajem.
  • Nie powinno się zmieniać żadnych danych wejściowych, których używają twoje komponenty do renderowania. Obejmuje to właściwości, stan i kontekst. Aby zaktualizować widok, ustaw stan zamiast modyfikować istniejące obiekty.
  • Staraj się wyrażać logikę swojego komponentu przez zwracany JSX. Gdy potrzeba “coś zmienić”, zazwyczaj powinno się zrobić to w obsłudze zdarzeń. W ostateczności można użyć useEffect.
  • Pisanie czystych funkcji wymaga trochę praktyki, ale uwalnia moc paradygmatu Reacta.

Wyzwanie 1 z 3:
Napraw zepsuty zegar

Ten komponent próbuje ustawić klasę CSS znacznika <h1> na "night" w godzinach od północy do szóstej rano oraz na "day" w pozostałych godzinach. Jednakże nie działa on poprawnie. Czy możesz to naprawić?

Możesz zweryfikować, czy twoje rozwiązanie działa, tymczasowo zmieniając strefę czasową na komputerze. Gdy obecny czas to pomiędzy północą a szóstą rano, zegar powinien mieć odwrócone kolory!

export default function Clock({ time }) {
  let hours = time.getHours();
  if (hours >= 0 && hours <= 6) {
    document.getElementById('time').className = 'night';
  } else {
    document.getElementById('time').className = 'day';
  }
  return (
    <h1 id="time">
      {time.toLocaleTimeString()}
    </h1>
  );
}