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.
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:
- Mounting rozpoczyna się w momencie wywołania
RenderDOM.render()
. Jest to moment narodzin instancji komponentu. W oparciu o dostarczoneprops
i bazowy stan zostanie wytworzona pierwsza wersja elementu React zwracanego przez komponent. Ten okres występuje tylko raz w cyklu życia komponentu. - Updating to najdłuższy okres w życiu każdego komponentu, powtarzany wielokrotnie. Zaczyna się przy pierwszej zmianie w
props
czystate
, 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. - 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:
- Render skupia metody czyste, pozbawione efektów ubocznych. Do ich obowiązków należy synchronizacja
state
zprops
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. - 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.
- 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 constructor
a 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ć constructor
a.
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 constructor
ze 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 kuzynacomponentDidMount()
- Gdy
shouldComponentUpdate
zwrócifalse
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: