Stan i cykl życia
W tym poradniku wprowadzimy pojęcie stanu (ang. state) i cyklu życia (ang. lifecycle) komponentu reactowego. Więcej informacji na ten temat znajdziesz w szczegółowej dokumentacji API komponentów.
Wróćmy do przykładu tykającego zegara z jednej z poprzednich lekcji. W sekcji “Renderowanie elementów” nauczyliśmy się tylko jednego sposobu aktualizowania interfejsu aplikacji. Aby zmienić wynik renderowania, wywołujemy funkcję root.render():
const root = ReactDOM.createRoot(document.getElementById('root'));
function tick() {
const element = (
<div>
<h1>Witaj, świecie!</h1>
<h2>Aktualny czas: {new Date().toLocaleTimeString()}.</h2>
</div>
);
root.render(element);}
setInterval(tick, 1000);W tym rozdziale dowiemy się, jak sprawić, by komponent Clock był w pełni hermetyczny i zdatny do wielokrotnego użytku. Wyposażymy go we własny timer, który będzie aktualizował się co sekundę.
Zacznijmy od wyizolowania kodu, który odpowiada za wygląd zegara:
const root = ReactDOM.createRoot(document.getElementById('root'));
function Clock(props) {
return (
<div> <h1>Witaj, świecie!</h1> <h2>Aktualny czas: {props.date.toLocaleTimeString()}.</h2> </div> );
}
function tick() {
root.render(<Clock date={new Date()} />);}
setInterval(tick, 1000);Brakuje jeszcze fragmentu, który spełniałby kluczowe założenie: inicjalizacja timera i aktualizowanie UI co sekundę powinny być zaimplementowane w komponencie Clock.
Idealnie byłoby móc napisać tylko tyle i oczekiwać, że Clock zajmie się resztą:
root.render(<Clock />);Aby tak się stało, musimy dodać do komponentu “stan”.
Stan przypomina trochę atrybuty (ang. props), jednak jest prywatny i w pełni kontrolowany przez dany komponent.
Przekształcanie funkcji w klasę
Proces przekształcania komponentu funkcyjnego (takiego jak nasz Clock) w klasę można opisać w pięciu krokach:
- Stwórz klasę zgodną ze standardem ES6 o tej samej nazwie i odziedzicz po klasie
React.Componentprzy pomocy słowa kluczowegoextend. - Dodaj pustą metodę o nazwie
render(). - Przenieś ciało funkcji do ciała metody
render(). - W
render()zamień wszystkiepropsnathis.props. - Usuń starą deklarację funkcji.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Witaj, świecie!</h1>
<h2>Aktualny czas: {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}Komponent Clock przestał już być funkcją i od teraz jest klasą.
Metoda render zostanie automatycznie wywołana przy każdej zmianie. Dopóki będziemy renderować <Clock /> do tego samego węzła drzewa DOM, dopóty używana będzie jedna i ta sama instancja klasy Clock. Pozwala to na skorzystanie z dodatkowych funkcjonalności, takich jak lokalny stan czy metody cyklu życia komponentu.
Dodawanie lokalnego stanu do klasy
Przenieśmy teraz date z atrybutów do stanu w trzech krokach:
- Zamień wystąpienia
this.props.datenathis.state.datew ciele metodyrender():
class Clock extends React.Component {
render() {
return (
<div>
<h1>Witaj, świecie!</h1>
<h2>Aktualny czas: {this.state.date.toLocaleTimeString()}.</h2> </div>
);
}
}- Dodaj konstruktor klasy i zainicjalizuj w nim pole
this.state:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()}; }
render() {
return (
<div>
<h1>Witaj, świecie!</h1>
<h2>Aktualny czas: {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}Zwróć uwagę na argument props przekazywany do konstruktora bazowego za pomocą specjalnej funkcji super():
constructor(props) {
super(props); this.state = {date: new Date()};
}Komponenty klasowe zawsze powinny przekazywać props do konstruktora bazowego.
- Usuń atrybut
datez elementu<Clock />:
root.render(<Clock />);Timer dodamy do komponentu nieco później.
W rezultacie powinniśmy otrzymać następujący kod:
class Clock extends React.Component {
constructor(props) { super(props); this.state = {date: new Date()}; }
render() {
return (
<div>
<h1>Witaj, świecie!</h1>
<h2>Aktualny czas: {this.state.date.toLocaleTimeString()}.</h2> </div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);Teraz sprawimy, by komponent Clock uruchomił własny timer i aktualizował go co sekundę.
Dodawanie metod cyklu życia do klasy
W aplikacjach o wielu komponentach istotne jest zwalnianie zasobów przy niszczeniu każdego z komponentów.
Chcielibyśmy uruchamiać timer przy każdym pierwszym wyrenderowaniu komponentu Clock do drzewa DOM. W Reakcie taki moment w cyklu życia komponentu nazywamy “montowaniem” (ang. mounting).
Chcemy również resetować timer za każdym razem, gdy DOM wygenerowany przez Clock jest usuwany z dokumentu. W Reakcie taki moment nazywamy to “odmontowaniem” (ang. unmounting) komponentu.
W klasie możemy zadeklarować specjalne metody, które będą uruchamiały kod w momencie montowania i odmontowywania komponentu:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() { }
componentWillUnmount() { }
render() {
return (
<div>
<h1>Witaj, świecie!</h1>
<h2>Aktualny czas: {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}Takie metody nazywamy “metodami cyklu życia”.
Metoda componentDidMount() uruchamiana jest po wyrenderowaniu komponentu do drzewa DOM. To dobre miejsce na inicjalizację timera:
componentDidMount() {
this.timerID = setInterval( () => this.tick(), 1000 ); }Zwróć uwagę, że identyfikator timera zapisujemy bezpośrednio do this (this.timerID).
Mimo że this.props jest ustawiane przez Reacta, a this.state jest specjalnym polem, to nic nie stoi na przeszkodzie, aby stworzyć dodatkowe pola, w których chcielibyśmy przechowywać wartości niezwiązane bezpośrednio z przepływem danych (jak nasz identyfikator timera).
Zatrzymaniem timera zajmie się metoda cyklu życia zwana componentWillUnmount():
componentWillUnmount() {
clearInterval(this.timerID); }Na koniec zaimplementujemy metodę o nazwie tick(), którą komponent Clock będzie wywoływał co sekundę.
Użyjemy w niej this.setState(), aby zaplanować aktualizację lokalnego stanu komponentu:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() { this.setState({ date: new Date() }); }
render() {
return (
<div>
<h1>Witaj, świecie!</h1>
<h2>Aktualny czas: {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);Teraz timer powinien już tykać co sekundę.
Podsumujmy, co dzieje się w powyższym kodzie i w jakiej kolejności wywoływane są metody:
- Kiedy element
<Clock />przekazywany jest do funkcjiroot.render(), React wywołuje konstruktor komponentuClock. Jako żeClockbędzie wyświetlać aktualny czas, musi on zainicjalizowaćthis.stateobiektem zawierającym aktualną datę. Później ten stan będzie aktualizowany. - Następnie React wywołuje metodę
render()komponentuClock. W ten sposób uzyskuje informację, co powinno zostać wyświetlone na stronie. Gdy otrzyma odpowiedź, odpowiednio aktualizuje drzewo DOM. - Po wyrenderowaniu komponentu
Clockdo drzewa DOM, React wywołuje metodę cyklu życia o nazwiecomponentDidMount(). W jej ciele komponentClockprosi przeglądarkę o zainicjalizowanie nowego timera, który będzie wywoływać metodętick()co sekundę. - Co sekundę przeglądarka wywołuje metodę
tick(). W jej ciele komponentClockżąda aktualizacji UI poprzez wywołanie metodysetState(), przekazując jako argument obiekt z aktualnym czasem. Dzięki wywołaniusetState()React wie, że zmienił się stan i że może ponownie wywołać metodęrender(), by dowiedzieć się, co powinno zostać wyświetlone na ekranie. Tym razem wartość zmiennejthis.state.datew ciele metodyrender()będzie inna, odpowiadająca nowemu czasowi - co React odzwierciedli w drzewie DOM. - Jeśli kiedykolwiek komponent
Clockzostanie usunięty z drzewa DOM, React wywoła na nim metodę cyklu życia o nazwiecomponentWillUnmount(), zatrzymując tym samym timer.
Poprawne używanie stanu
Są trzy rzeczy, które musisz wiedzieć o setState().
Nie modyfikuj stanu bezpośrednio
Na przykład, poniższy kod nie spowoduje ponownego wyrenderowania komponentu:
// Źle
this.state.comment = 'Witam';Zamiast tego używaj setState():
// Dobrze
this.setState({comment: 'Witam'});Jedynym miejscem, w którym wolno Ci użyć this.state jest konstruktor klasy.
Aktualizacje stanu mogą dziać się asynchroniczne
React może zgrupować kilka wywołań metody setState() w jedną paczkę w celu zwiększenia wydajności aplikacji.
Z racji tego, że zmienne this.props i this.state mogą być aktualizowane asynchronicznie, nie powinno się polegać na ich wartościach przy obliczaniu nowego stanu.
Na przykład, poniższy kod może nadpisać counter błędną wartością:
// Źle
this.setState({
counter: this.state.counter + this.props.increment,
});Aby temu zaradzić, wystarczy użyć alternatywnej wersji metody setState(), która jako argument przyjmuje funkcję zamiast obiektu. Funkcja ta otrzyma dwa argumenty: aktualny stan oraz aktualne atrybuty komponentu.
// Dobrze
this.setState((state, props) => ({
counter: state.counter + props.increment
}));W powyższym kodzie użyliśmy funkcji strzałkowej, lecz równie dobrze moglibyśmy użyć zwykłej funkcji:
// Dobrze
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});Aktualizowany stan jest scalany
Gdy wywołujesz setState(), React scala (ang. merge) przekazany obiekt z aktualnym stanem komponentu.
Na przykład, gdyby komponent przechowywał w stanie kilka niezależnych zmiennych:
constructor(props) {
super(props);
this.state = {
posts: [], comments: [] };
}można byłoby je zaktualizować niezależnie za pomocą osobnych wywołań metody setState():
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts });
});
fetchComments().then(response => {
this.setState({
comments: response.comments });
});
}Scalanie jest płytkie (ang. shallow), tzn. this.setState({comments}) nie zmieni this.state.posts, lecz całkowicie nadpisze wartość this.state.comments.
Dane płyną z góry na dół
Ani komponenty-rodzice, ani ich dzieci nie wiedzą, czy jakiś komponent posiada stan, czy też nie. Nie powinny się również przejmować tym, czy jest on funkcyjny, czy klasowy.
Właśnie z tego powodu stan jest nazywany lokalnym lub enkapsulowanym. Nie mają do niego dostępu żadne komponenty poza tym, który go posiada i modyfikuje.
Komponent może zdecydować się na przekazanie swojego stanu w dół struktury poprzez atrybuty jego komponentów potomnych:
<FormattedDate date={this.state.date} />Komponent FormattedDate otrzyma date jako atrybut i nie będzie w stanie rozróżnić, czy pochodzi on ze stanu lub jednego z atrybutów komponentu Clock, czy też został przekazany bezpośrednio przez wartość:
function FormattedDate(props) {
return <h2>Aktualny czas: {props.date.toLocaleTimeString()}.</h2>;
}Taki przepływ danych nazywany jest powszechnie jednokierunkowym (ang. unidirectional) lub “z góry na dół” (ang. top-down). Stan jest zawsze własnością konkretnego komponentu i wszelkie dane lub części UI, powstałe w oparciu o niego, mogą wpłynąć jedynie na komponenty znajdujące się “poniżej” w drzewie.
Wyobraź sobie, że drzewo komponentów to wodospad atrybutów, a stan każdego z komponentów to dodatkowe źródło wody, które go zasila, jednocześnie spadając w dół wraz z resztą wody.
Aby pokazać, że wszystkie komponenty są odizolowane od reszty, stwórzmy komponent App, który renderuje trzy elementy <Clock>:
function App() {
return (
<div>
<Clock /> <Clock /> <Clock /> </div>
);
}Każdy Clock tworzy swój własny timer i aktualizuje go niezależnie od pozostałych.
W aplikacjach reactowych to, czy komponent ma stan, czy nie, jest tylko jego szczegółem implementacyjnym, który z czasem może ulec zmianie. Możesz dowolnie używać bezstanowych komponentów (ang. stateless components) wewnątrz tych ze stanem (ang. stateful components), i vice versa.