Kategorie
Archiwum Newslettera

Na czym polega asynchroniczność w React?

JavaScript wyróżnia się na tle wszystkich innych języków. Jest z natury asynchroniczny. Wiedzą o tym dobrze twórcy Reakta i doskonale wykorzystują to w swojej bibliotece.

JavaScript wyróżnia się na tle wszystkich innych języków. Jest z natury asynchroniczny. Wiedzą o tym dobrze twórcy Reakta i doskonale wykorzystują to w swojej bibliotece.

Dziś wracamy do fundamentów Reakta. Warto sobie bardzo dobrze utrwalić poniższe wiadomości. Zrozumienie tych zagadnień jest trampoliną do następnego poziomu zaawansowania. Żeby było jasne: według mnie możesz porzucić marzenia o zostaniu Regular React Developerem, jeśli nie wiesz, o co w tym chodzi.

Nie zapominaj o asynchronicznej naturze Reakta!

React, tak jak sam JavaScript, również jest asynchroniczny. Co to znaczy? Nie wszystko dzieje się w tej kolejności w jakiej to zapisujemy. Znasz promisy? React pomaga się nam nimi posługiwać dając do dyspozycji hooka useEffect i zdarzenia, czyli eventy. Aby otrzymać wartość z promisa, należy wykonać na nim metodę then i przekazać do niej callback, czyli funkcję, która będzie wywołana, kiedy promise się wykona. No właśnie „kiedy się wykona” to słowo klucz. Czytając komponent od góry do dołu można odnieść wrażenie, że wszystko zawsze dzieje się linijka po linijce. Nic bardziej mylnego!

Czy się wykona, czy się nie wykona, wyświetlić coś trzeba

Przeglądarkowa funkcja fetch zwraca właśnie promisa. Czekam aż, serwer zwróci nam jakąś odpowiedź i zapisuję ją do stanu za pomocą setState:

const RandomBeer = () => {
  const [state, setState] = useState('empty');

  useEffect(() => {
    fetch(url)
      .then((newState) => setState(newState))
  }, []);

  return <p>RandomBeer: {state}</p>
}

Zwróć uwagę, że dopóki nie otrzymam odpowiedzi z serwera to wyświetlony zostaje paragraf z wpisanym do niego słowem „empty”. Równie dobrze mógłbym użyć jakiegoś ifa i na podstawie odpowiedniego warunku wyświetlić loader, jakieś kręcące się kółko.

Komponenty to funkcje

Komponenty to funkcje, które zwracają JSX, czyli wywołania innych komponentów itd. Aby otrzymać wartość zwróconą przez komponent, należy go wywołać. Dopóki go nie wywołamy, nic nowego nie otrzymamy. Dlatego, aby pokazać aktualne dane na stronie, komponent musi zostać wywołany ponownie a jego wartość (JSX) obliczona i przekazana do drzewa DOM.

W powyższym przykładzie z komponentem RandomBeer tak właśnie się dzieje! Komponent nie będzie czekać na promisa. Zwróci wartość bez jego odpowiedzi! A kiedy promise zostanie wykonany, komponent wywoła się po raz kolejny. O to właśnie dbają hooki useEffect i useState. Dzięki temu, że setState zostaje przekazany do jako callback, komponent dokładnie wie kiedy wywołać się ponownie. W skrócie: każde wywołanie setState, powoduje ponowne obliczenie komponentu. Uważaj, aby nie zapętlić 😂

useState też jest asynchroniczny!

Mało tego! useState również tak działa! Asynchronicznie! W przykładzie poniżej spróbuj wykonać setState kilka razy na podstawie danego stanu. Jeśli przekażesz wartość prosto z hooka (tutaj zmienna state), to okaże się, że wartość zmieniła się pozornie tylko raz i komponent wypisze 1. A wydaje się, że powinno być 3. Dlaczego tak się dzieje? Ponieważ w każdej linijce zmienna state ma wartość dokładnie 0, tak jak ją zainicjowałem.

const UglyNumber = () => {
  const [state, setState] = useState(0);

  useEffect(() => {
    setState(state + 1);
    setState(state + 1);
    setState(state + 1);
  }, []);

  return <p>Ugly Number: {state}</p>
}

Przeanalizujmy ten komponent – UglyNumber! W pierwszej linii pobierana jest wartość state, czyli 0. Następnie w linijce czwartej następuje pierwsze wywołanie setState(state + 1) co ustawia wartość nowego state na 1. Ale czy w zmiennej state będzie teraz wartość 1? Nie! Ponieważ pobrałem ją na początku jako 0. Dopiero przy następnym wykonaniu komponentu wartość będzie 1. A przecież nie mogę się cofnąć do pierwszej lini, ponieważ muszę wykonać linię piątą! Zatem wykonuję następne setState(state + 1). Zmienna state ma ciągle wartość 0, więc ponownie ustawiam nowy stan na 1. I tak dzieje się również przy trzecim wywołaniu. W konsekwencji wykonuję trzy razy tę samą czynność – ustawiam nowy state na 1.

Aby kod zadziałał, tak jak tego oczekuję, czyli dodać trzy razy jedynkę i otrzymać ostatecznie 3, należy przekazać funkcję jako argument setState. Dokładnie tak jak poniżej w przykładzie z komponentem PrettyNumber.

const PrettyNumber = () => {
  const [state, setState] = useState(0);

  useEffect(() => {
    setState((prevState) => prevState + 1);
    setState((prevState) => prevState + 1);
    setState((prevState) => prevState + 1);
  }, []);

  return <p>Pretty Number: {state}</p>
}

Dzięki takiej konstrukcji w zmiennej prevState, znajduje się zawsze aktualny stan. Takiego zachowani oczekiwał niedoświadczony programista w przykładzie pierwszym.

Jakie błędy popełniają początkujący?

Trenowałem przyszłych programistów na bootcampach. Dla niektórych nie było to pierwsze zetknięcie z kodem. Mimo tego wszyscy popełniali te same błędy:

  • Używanie zwykłych zmiennych do przechowywania stanu zamiast używać useState. Po co mi hook jak mam zmienne let?
  • Wywoływanie setState w ciele komponentu zamiast wewnątrz useEffect lub handlerach zdarzeń np. onClick na buttonie.
  • Wielokrotne wywołania setState bez przekazywania funkcji jako argument, co prowadzi do nieoczekiwanych wyników.
  • Niezrozumienie jak działają promisy