Zatłoczone skrzyżowanie w Tokio

Cykl życia komponentu – PPwRJS #9

W czym każdy z nas, programistów, przypomina komponent React? Człowiek przechodzi przez dzieciństwo, dojrzałość i starość. Tak się składa, że bardzo podobny cykl życia charakteryzuje komponenty. Możemy w ich przypadku wydzielić okresy Mounting (dzieciństwo), Updating (dojrzałość) i Unmounting (starość). Taki systematyczny podział, wspólny dla każdego przedstawiciela swojego rodzaju, pozwala podzielić obowiązki, którym każda jednostka będzie musiała sprostać w poszczególnych okresach swojego żywota. Zdecydowanie łatwiej poradzić sobie w skomplikowanym świecie z informacją, że nauka chodzenia (w przypadku ludzi) czy inicjalizacja stanu (w przypadku komponentów) powinna być jedną z pierwszych spraw, którym poświęcimy uwagę. W internecie roi się od rad odnośnie do tego, jak radzić sobie z cyklem życia homo sapiens. Ja ograniczę się do złotych rad zapewniających właściwy rozwój Twoich komponentów.

Różne okresy, takie same fazy

Po raz kolejny (i na pewno nie ostatni) skorzystam z pomocy niezastąpionego Dana Abramova.

Diagram metod cyklu życia z podziałem na okresy i fazy (autor: Dan Abramov)

Dan wydzielił trzy fazy wewnątrz każdego z okresów. Samych metod jest, aż 7. Część z nich należy do dwóch okresów. To dużo abstrakcji jak na jedno zagadnienie. Dołożyłem wszelkich starań, aby w zrozumiały sposób, nakreślić Ci jak to wygląda w praktyce.

Zacznijmy od charakterystyki poszczególnych okresów cyklu życia:

  1. Mounting rozpoczyna się w momencie wywołania RenderDOM.render(). Jest to moment narodzin instancji komponentu. W oparciu o dostarczone props i bazowy stan zostanie wytworzona pierwsza wersja elementu React zwracanego przez komponent. Ten okres występuje tylko raz w cyklu życia komponentu.
  2. Updating to najdłuższy okres w życiu każdego komponentu, powtarzany wielokrotnie. Zaczyna się przy pierwszej zmianie w props czy state, co zwykle następuje zaraz po zakończeniu Mounting. W tym okresie obsługujemy główne funkcje komponentu, aktualizujemy dane, reagujemy na działania użytkownika itd.
  3. Unmounting następuje, gdy przyjdzie czas na pożegnanie się instancji z drzewem elementów. Zanim to nastąpi, należy zadbać o należyty porządek, co umożliwi sprawną pracę kolejnego pokolenia komponentów.

Teraz przejdźmy do wydzielonych przez Dana faz:

  1. Render skupia metody czyste, pozbawione efektów ubocznych. Do ich obowiązków należy synchronizacja state z props i szeroko rozumiane przygotowanie komponentu do nadchodzącego renderowania. Metody render phase muszą być pozbawione efektów ubocznych, ich wywoływanie doprowadziłoby do ponownego rozpoczęcia całej fazy.
  2. Pre-commit wypełnia krótki moment pomiędzy wywołaniem render a aktualizacją DOM. Możemy w tym czasie zapisać stan DOM przed aktualizacją, na wypadek gdybyśmy chcieli się do niego odwołać w commit phase.
  3. Commit, ostatnia z faz, obejmuje wszystko co dzieje się po aktualizacji DOM. To najlepszy moment na wywoływanie skutków ubocznych (zapytania do serwera, aktualizacje stanu).

Zanim zaczniemy omawianie poszczególnych metod, małe sprostowanie. Na diagramie widzicie stan metod cyklu życia zgodny z wersją React 16.3. Jeżeli wcześniej korzystałeś z cyklu życia, to na pewno zauważyłeś nieobecność metod componentWillMount, componentWillReceiveProps i componentWillUpdate. Zostały wysłane na wycieczkę w jedną stronę do krainy deprecated. Niech spoczywają w spokoju, nie chcę poświęcać czasu na ich omawianie…

Jednak tak się składa, że metoda componentWillMount jest obecnie wykorzystywana w trackerze kryptowalut:

// App.js
state = {
	...,
	matchedCryptos: [],
	...
}

componentWillMount() {
  this.setMatchedCryptos();
}	

setMatchedCryptos = debounce(() => {
    const cryptos = [...this.state.cryptos];

    function isMatched(phrase) {
      const regex = new RegExp(`\\b${phrase}.*\\b`, 'i');
      return function(crypto) {
        return Object.values(crypto).some(val => regex.test(val));
      };
    }

    const isMatchedWithSearchQuery = isMatched(this.state.searchQuery);
    const matchedCryptos = cryptos.filter(isMatchedWithSearchQuery);
    this.setState({ matchedCryptos });
}, 250);

W dużym skrócie: w ten sposób dokonywałem inicjalizacji wartości tablicy matchedCryptos, wyświetlanej przez CoinList, przed pierwszym renderem. Początkowy stan searchQuery to '', więc wszystkie obiekty obecne w state.cryptos trafiały do matchedCryptos.

Poszukamy zastępstwa dla componentWillMount wśród metod, które ostały się po ostatniej aktualizacji. Zobaczymy, czy obejdzie się bez zmian w logice.

Metody cyklu życia

constructor(props) [Okres: Mounting, Faza: Render]

Metoda dobrze znana z klas ES6. W przypadku komponentów React, służy do inicjalizacji początkowego stanu i wiązania metod z instancją komponentu. Użycie constructora nie jest jedynym sposobem na poradzenie sobie z tymi obowiązkami. Sam chętnie korzystam z innego sposobu na inicjalizację początkowego stanu, czyli pól klas ES7. Z wiązaniami metod możemy sobie poradzić deklarując je jako funkcje strzałkowe. W ten sposób leksykalne wyszukiwanie this wykona pracę za nas. Z powodu tych alternatyw natkniesz się na wiele komponentów, w których na próżno szukać constructora.

Jeżeli zdecydujesz się na korzystanie z tej metody, musisz pamiętać o wywołaniu w jej wnętrzu super(props). Dzięki temu props zostaną przekazane do React.Component, abstrakcyjnej klasy bazowej, którą rozszerza każdy komponent klasowy. React.Component wiąże props z instancją, co pozwala na odwoływanie się do tego obiektu za pomocą this.props w całym jej wnętrzu.

Co robimy w constructor?

  • Obowiązkowo wywołujemy super(props)
  • Inicjalizujemy stan komponentu (this.state = { ... })
  • Wiążemy metody z instancją komponentu

Czego nie robimy?

  • Wywołujemy efekty uboczne (AJAX, subskrypcje zdarzeń, dispatchowanie akcji)
  • Aktualizujemy stan przy pomocy this.setState()

Czy constructor to dobre zastępstwo dla componentWillMount? Niestety nie. Poprawna inicjalizacja w constructorze ogranicza się przypisywania wartości do literału obiektu (this.state = { ... }). Wywoływanie this.setState() nie wchodzi w grę. Szukamy dalej.


static getDerivedStateFromProps(nextProps, prevState) [Okres: Mounting/Updating, Faza: Render]

Jedna z dwóch nowych metod, wprowadzonych w aktualizacji 16.3. Występuje zarówno w okresie Mounting jak i Updating. Wywołuje ją przybycie do komponentu nowych propsów. W gDSFP porównujemy nowe propsy z dotychczasowym state. W ten sposób możemy zadecydować czy warto synchronizować stan przed renderem. gDSFP jest metodą statyczną, co wiąże się z brakiem dostępu do this wskazującego na instancję. Synchronizację stanu reprezentujemy za pomocą obiektu, który zwracamy, zamiast przekazywać go do this.setState().

Co robimy w getDerivedStateFromProps?

  • Synchronizujemy state z nowymi propsami

Czego nie robimy?

  • Wywołujemy efekty uboczne

shouldComponentUpdate(nextProps, nextState, nextContext) [Okres: Updating, Faza: Render]

Metoda, która pozwala przerwać cały okres Updating. Spoiler alert: za taką władzą idzie spora odpowiedzialność. Domyślnie React wymusza re-render przy każdej modyfikacji props, state lub context. Rzecz w tym, że zmiany w tych obiektach nie zawsze wiążą się ze zmianami widocznymi w interfejsie. W takiej sytuacji re-render komponentu jest potencjalnym marnotrawieniem zasobów. Jako że obecnie zasobów mamy pod dostatkiem, w 99% przypadków powinniśmy machnąć na to ręką. shouldComponentUpdate jest furtką na wypadek sytuacji nadzwyczajnych, np. obsługi list długich na setki pozycji czy rozbudowanych wykresów.

Jeżeli nie jesteś pewien, czy Twój problem zalicza się do sytuacji nadzwyczajnych, nie trać czasu na zastanawianie się nad analizą potencjalnych korzyści. Zmierz, ile tracisz na pominięciu tej metody. Jeżeli okaże się, że jest to wartość rzędu 1ms, to rozsądniej sobie odpuścić. Koszty utrzymania dodatkowego kodu nie uzasadniają tak znikomych korzyści.

Co do samej mechaniki shouldComponentUpdate: to, czy okres Updating będzie kontynuowany, jest zależne od wartości boolowskiej zwróconej przez tę metodę. Jak podpowiada nam nazwa metody, true wiąże się z kontynuacją, podczas gdy false przerywa okres Updating.

Co robimy w shouldComponentUpdate?

  • Podejmujemy decyzję, czy warto kontynuować okres Updating, mając na uwadze na realny wzrost wydajności (1% sytuacji)

Czego nie robimy?

  • Podejmujemy decyzję, czy warto kontynuować okres Updating, mając na uwadze niezauważalny wzrost wydajności (99% sytuacji)
  • Wywołujemy efekty uboczne
  • Aktualizujemy stan przy pomocy this.setState()

render() [Okres: Mounting/Updating, Faza: Render]

Nawet jeżeli ten wpis jest Twoim pierwszym kontaktem z cyklem życia, na pewno dobrze znasz tę metodę. Jest ona punktem kulminacyjnym fazy renderowania, której nazwa pochodzi właśnie od tej metody. Zadaniem render jest zwrócenie elementu React, który biblioteka użyje do aktualizacji DOM.

Co robimy w render?

  • Zwracamy element React

Czego nie robimy?

  • Wywołujemy efekty uboczne
  • Aktualizujemy stan przy pomocy this.setState()

getSnapshotBeforeUpdate(prevState, prevProps) [Okres: Updating, Faza: Pre-commit]

Druga nowość z wersji 16.3. Jedyna metoda należąca do fazy pre-commit. Jest wywoływana na moment przed aktualizacją DOM, po zakończeniu wykonywania render(). Pozwala przekazać dane związane z stanem DOM sprzed aktualizacji do kolejnej metody cyklu życia w okresie Updating czyli componentDidUpdate. Praktyczne zastosowanie? W dokumentacji jest podany przykład z dostosowywaniem scrolla do zmieniającej się listy.

Co do samej mechanik gSBU, zwracamy wartość, która zostanie przypisana przez Reacta do parametru snapshot w cDU.

Co robimy w getSnapshotBeforeUpdate?

  • Robimy zrzut z stanu DOM przed aktualizacją na użytek componentDidUpdate

Czego nie robimy?

  • Wywołujemy efekty uboczne
  • Aktualizujemy stan przy pomocy this.setState()

componentDidMount() [Okres: Mounting, Faza: Commit]

Metoda wywoływana po pierwszej aktualizacji DOM. Świetnie nadaje się na wczytanie danych z serwera oraz nawiązanie kontaktu z Reduxem (o tym w przyszłości). Mamy możliwość wywołania this.setState(), ale wiąże się to z dodatkowym rerenderem, co może prowadzić do problemów z wydajnością. Jak radzi Bartosz Szczeciński, lepiej unikać aktualizacji stanu poza synchronizacją danych w obsłudze AJAX. Nie jest to żelazna zasada, wyjątki wskazuje dokumentacja.

Co robimy w componentDidMount?

  • Wczytujemy dane z serwera i synchronizujemy z nimi stan (AJAX)
  • Tworzymy subskrypcje zdarzeń Reduxa
  • Dispatchujemy akcje Reduxa

Czego nie robimy?

  • Aktualizujemy stan poza callbackami AJAX (z małymi odstępstwami)

Czy componentDidMount zastąpi componentWillMount? Niestety nie. Blokuje nas wspomniane ograniczenie wywołań setState do obsługi AJAX.

Czyżby faktycznie szykowały się zmiany w logice, z której korzystałem do tej pory?

componentDidUpdate(prevProps, prevState, snapshot) [Okres: Updating, Faza: Commit]

Metoda bardzo podobna do componentDidMount, tyle że wywoływana znacznie częściej. W componentDidUpdate otrzymuje referencje do poprzednich props i state. Dzięki temu możemy sprawdzić czy wywołanie zaplanowanych efektów ubocznych jest w ogóle potrzebne. Może dysponujemy tymi samymi danymi do zapytania AJAX, co przy poprzednim wywołaniu componentDidUpdate? Warto to zweryfikować. Nie ma sensu drugi raz uzyskiwać tej samej odpowiedzi. Zwłaszcza, że łatwo wkopać się w nieskończoną pętlę re-renderów.

Co robimy w componentDidUpdate?

  • Wywołujemy efekty uboczne (po upewnieniu się, że props/state uległo zmianie)

Czego nie robimy?

  • Aktualizujemy stan poza callbackami AJAX (analogicznie do componentDidMount)

Pamiętaj, że componentDidUpdate nie zostanie wywoływane w dwóch przypadkach:

  • Po pierwszym wywołaniu render(), to broszka jego kuzyna componentDidMount()
  • Gdy shouldComponentUpdate zwróci false

componentWillUnmount() [Unmouting, Commit phase]

Ostatnia metoda na liście, wywoływana na moment przed odejściem instancji w niepamięć. W cWU mamy sposobność do załatwienia ostatnich sprawunków: wyczyszczenia timerów i interwałów, anulowanie zapytań ajax, usunięcia subskrypcji Redux itd.

Co robimy w componentWillUnmount?

  • Sprzątamy po komponencie

Czego nie robimy?

  • Wywołujemy skutki uboczne

Zastępstwo czy emerytura

„Krótki” przegląd metod cyklu życia za nami. Wygląda na to, że żadna z metod okresu Mounting nie nadaje się do ustawienia początkowego stanu this.state.matchedCryptos za pośrednictwem wywołania this.setMatchedCryptos(). Wniosek jest prosty: trzeba zrezygnować z takiej logiki. Zmienimy wyjściowy stan matchedCryptos na null zamiast []. Ten sam efekt, co poprzednio, (cryptos === this.state.cryptos przy pierwszym renderze) otrzymamy za pomocą następującej zmiany:

// Wcześniej
<CoinList cryptos={this.state.matchedCryptos} />
	
// Teraz
<CoinList cryptos={this.state.matchedCryptos !== null ? this.state.matchedCryptos : this.state.cryptos} />

Takie rozwiązanie nie zostanie z nami na długo. Kolejny wpis przyniesie kolejną przebudowę trackera. Wszystko za sprawą AJAX i pozyskiwania danych w czasie rzeczywistym z zewnętrznego API.

Goodies

Zanim rozstaniemy się z cyklem życia, mam dla Was trochę ekstrasów:

  • Symulator metod cyklu życia to niesamowite narzędzie. Zachwyca wartością edukacyjną i estetyczną. Mamy możliwość zasymulowania cyklu życia komponentu, wywołania wszystkich jego metod w odpowiedniej kolejności, wraz z operacjami, które możemy wykonywać w ich wnętrzu. Zajrzyj koniecznie!
  • Uderstanding React – Life cycle methods (part 1) i (part 2) – anglojęzyczne artykuły z cieszącej się niesamowitą popularnością serii Understanding React autorstwa naszego rodaka Bartosza Szczecińskiego. Krótko, jasno i na temat – polecam gorąco wszystkie artykuły z serii, są dla mnie kopalnią wiedzy.
  • Update on async rendering – artykuł wyjaśniający decyzje o wprowadzeniu nowych i wycofaniu części starych metod cyklu życia w aktualizacji 16.3.
  • Podsumowanie

    Nabytą wiedzę o cyklu życia komponentu użyjemy w praktyce już za tydzień podczas zmagań z AJAX w React. Planowany efekt tego pojedynku to transformacja prototypu w aplikację z prawdziwego zdarzenia. W ramach ćwiczenia zastanów się, z jakich metod zrobimy użytek.

    Swoją drogą, nadchodzący wpis będzie ostatnim w serii Pierwszy projekt w React JS. Zgodnie z obietnicą, którą złożyłem w pierwszym wpisie, w sidebarze pojawiła się ankieta. Oddaj swój głos i zadecyduj o czym mam pisać w najbliższym czasie. Na tę chwilę najbardziej interesują mnie wzorce w React (np. render props) oraz Redux, stąd takie opcje do wyboru.

    Kod źródłowy projektu jest dostępny tutaj.

    Podobał Ci się dzisiejszy artykuł? Udostępnij go w Twoich ulubionych mediach społecznościowych. Może znajdzie się ktoś, dla kogo również będzie wartościowy.

    Bądź na bieżąco z serią Pierwszy projekt w ReactJS. Wystarczy polubić fanpage AlgoSmart na fejsie, obserwować mój profil na Twitterze i regularnie odwiedzać portal Polski Front-end.

    Do zobaczenia za tydzień :).

    Zdjęcie tytułowe autorstwa: unsplash-logoCory Schadt

    Marcin Czarkowski

    Cześć! Ja nazywam się Marcin Czarkowski, a to jest AlgoSmart - blog, na którym dzielę się wiedzą o React.js, JavaScript oraz CSS.