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.Component
przy pomocy słowa kluczowegoextend
. - Dodaj pustą metodę o nazwie
render()
. - Przenieś ciało funkcji do ciała metody
render()
. - W
render()
zamień wszystkieprops
nathis.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.date
nathis.state.date
w 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
date
z 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 żeClock
będzie wyświetlać aktualny czas, musi on zainicjalizowaćthis.state
obiektem 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
Clock
do drzewa DOM, React wywołuje metodę cyklu życia o nazwiecomponentDidMount()
. W jej ciele komponentClock
prosi 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.date
w ciele metodyrender()
będzie inna, odpowiadająca nowemu czasowi - co React odzwierciedli w drzewie DOM. - Jeśli kiedykolwiek komponent
Clock
zostanie 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.