Stan jako migawka
Zmienne stanu mogą wyglądać jak zwykłe zmienne javascriptowe, które można odczytywać i zapisywać. Jednak stan zachowuje się bardziej jak migawka. Ustawienie go nie zmienia zmiennej stanu, którą już masz, ale zamiast tego wyzwala ponowne renderowanie.
W tej sekcji dowiesz się
- Jak ustawienie stanu wyzwala ponowne renderowanie
- Kiedy i jak stan się aktualizuje
- Dlaczego stan nie aktualizuje się natychmiast po jego ustawieniu
- Jak procedury obsługi zdarzeń uzyskują dostęp do “migawki” stanu
Ustawianie stanu wyzwala ponowne renderowanie
Możesz myśleć, że twój interfejs użytkownika zmienia się bezpośrednio w odpowiedzi na zdarzenia użytkownika, takie jak kliknięcie. W Reakcie działa to nieco inaczej. Na poprzedniej stronie mogliśmy zobaczyć, że ustawienie stanu wysyła żądanie ponownego renderowania do Reacta. Oznacza to, że aby interfejs zareagował na zdarzenie, musisz zaktualizować stan.
Gdy naciśniesz “Wyślij” w poniższym przykładzie, wywołanie setIsSent(true)
informuje Reacta, aby ponownie wyrenderował interfejs użytkownika:
import { useState } from 'react'; export default function Form() { const [isSent, setIsSent] = useState(false); const [message, setMessage] = useState('Cześć!'); if (isSent) { return <h1>Twoja wiadomość jest w drodze!</h1> } return ( <form onSubmit={(e) => { e.preventDefault(); setIsSent(true); sendMessage(message); }}> <textarea placeholder="Wiadomość" value={message} onChange={e => setMessage(e.target.value)} /> <button type="submit">Wyślij</button> </form> ); } function sendMessage(message) { // ... }
Oto co dzieje się, gdy klikniesz przycisk:
- Wykonuje się procedura obsługi zdarzenia
onSubmit
. - Wywołanie
setIsSent(true)
ustawiaisSent
natrue
i dodaje do kolejki nowe renderowanie. - React ponownie renderuje komponent zgodnie z nową wartością
isSent
.
Przyjrzyjmy się bliżej związkowi między stanem a renderowaniem.
Renderowanie tworzy migawkę danego momentu
“Renderowanie” oznacza, że React wywołuje twój komponent, który jest funkcją. Zwracany przez tę funkcję JSX jest jak migawka interfejsu użytkownika w danym momencie. Jego właściwości, procedury obsługi zdarzeń i zmienne lokalne zostały obliczone na podstawie stanu w momencie renderowania.
W przeciwieństwie do fotografii czy klatki filmu, zwracana przez ciebie “migawka” interfejsu użytkownika jest interaktywna. Zawiera logikę, taką jak procedury obsługi zdarzeń, które określają, co się stanie w odpowiedzi na dane wejściowe. React aktualizuje ekran, aby dopasować go do tej migawki i podłącza procedury obsługi zdarzeń. W rezultacie naciśnięcie przycisku uruchomi procedurę obsługi kliknięcia z twojego JSX.
Kiedy React ponownie renderuje komponent:
- React ponownie wywołuje twoją funkcję.
- Twoja funkcja zwraca nową migawkę JSX.
- React następnie aktualizuje ekran, aby dopasować go do migawki zwróconej przez twoją funkcję.
Autor ilustracji Rachel Lee Nabors
Będąc pamięcią komponentu, stan nie jest jak zwykła zmienna, która znika po zakończeniu działania funkcji. Stan, tak naprawdę, “żyje” w samym Reakcie (jakby był na półce!) poza twoją funkcją. Kiedy React wywołuje twój komponent, przekazuje ci migawkę stanu dla tego konkretnego renderowania. Twój komponent zwraca migawkę interfejsu użytkownika ze świeżym zestawem właściwości i procedur obsługi zdarzeń w swoim JSX, gdzie wszystko jest obliczone przy użyciu wartości stanu z tego renderowania!
Autor ilustracji Rachel Lee Nabors
Oto mały eksperyment, pokazujący jak to działa. W tym przykładzie można by się spodziewać, że kliknięcie przycisku “+3” zwiększy licznik trzykrotnie, ponieważ wywołuje on setNumber(number + 1)
trzy razy.
Zobacz, co się stanie po kliknięciu przycisku “+3”:
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 1); setNumber(number + 1); setNumber(number + 1); }}>+3</button> </> ) }
Zauważ, że number
zwiększa się tylko o jeden za każdym kliknięciem!
Ustawienie stanu zmienia go dopiero w następnym renderowaniu. Podczas pierwszego renderowania number
miał wartość 0
. Dlatego w procedurze obsługi onClick
dla tego renderowania wartość number
nadal wynosi 0
, nawet po wywołaniu setNumber(number + 1)
:
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
Oto co procedura obsługi kliknięcia tego przycisku każe zrobić Reactowi:
setNumber(number + 1)
:number
wynosi0
, więc to inaczejsetNumber(0 + 1)
.- React przygotowuje się do zmiany
number
na1
przy następnym renderowaniu.
- React przygotowuje się do zmiany
setNumber(number + 1)
:number
wynosi0
, więc to inaczejsetNumber(0 + 1)
.- React przygotowuje się do zmiany
number
na1
przy następnym renderowaniu.
- React przygotowuje się do zmiany
setNumber(number + 1)
:number
wynosi0
, więc to inaczejsetNumber(0 + 1)
.- React przygotowuje się do zmiany
number
na1
przy następnym renderowaniu.
- React przygotowuje się do zmiany
Mimo że wywołano setNumber(number + 1)
trzy razy, w procedurze obsługi zdarzeń tego renderowania number
zawsze wynosi 0
, więc trzy razy ustawiono stan na 1
. Dlatego po zakończeniu działania procedury obsługi zdarzeń, React ponownie renderuje komponent z number
równym 1
, a nie 3
.
Możesz to sobie również zwizualizować, podstawiając w myślach wartości zmiennych stanu w swoim kodzie. Ponieważ zmienna stanu number
ma wartość 0
dla tego renderowania, jego procedura obsługi zdarzeń wygląda tak:
<button onClick={() => {
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
}}>+3</button>
Dla następnego renderowania number
wynosi 1
, więc procedura obsługi kliknięcia tego renderowania wygląda tak:
<button onClick={() => {
setNumber(1 + 1);
setNumber(1 + 1);
setNumber(1 + 1);
}}>+3</button>
Dlatego ponowne kliknięcie przycisku ustawi licznik na 2
, następnie na 3
przy kolejnym kliknięciu i tak dalej.
Stan w czasie
To było ciekawe. Spróbuj teraz zgadnąć, co wyświetli alert po kliknięciu tego przycisku:
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); alert(number); }}>+5</button> </> ) }
Jeśli zastosujesz metodę podstawiania wspomnianą wcześniej, możesz zgadnąć, że alert pokaże “0”:
setNumber(0 + 5);
alert(0);
A co, jeśli dodasz timer do alertu, tak aby wyświetlił się dopiero po ponownym wyrenderowaniu komponentu? Pokaże on “0” czy “5”? Zgadnij!
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setTimeout(() => { alert(number); }, 3000); }}>+5</button> </> ) }
Zaskoczenie? Jeśli zastosujesz metodę podstawiania, zobaczysz “migawkę” stanu przekazaną do alertu.
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);
Stan przechowywany w Reakcie może zmienić się do czasu wyświetlenia alertu, ale alert ten został zaplanowany przy użyciu migawki stanu z momentu interakcji użytkownika!
Wartość zmiennej stanu nigdy nie zmienia się w obrębie pojedynczego renderowania, nawet jeśli kod jej procedury obsługi zdarzeń jest asynchroniczny. Wewnątrz metody onClick
tego renderowania, wartość zmiennej number
nadal wynosi 0
, nawet po wywołaniu setNumber(number + 5)
. Jej wartość została “utrwalona” w momencie, gdy React “zrobił migawkę” interfejsu użytkownika poprzez wywołanie twojego komponentu.
Oto przykład pokazujący, jak to sprawia, że procedury obsługi zdarzeń są mniej podatne na błędy związane z czasem wykonania. Poniżej znajduje się formularz, który wysyła wiadomość z pięciosekundowym opóźnieniem. Wyobraź sobie taki scenariusz:
- Naciskasz przycisk “Wyślij”, aby wysłać wiadomość “Cześć” do Alice.
- Zanim upłynie pięciosekundowe opóźnienie, zmieniasz wartość pola “Do” na “Bob”.
Jak myślisz, co wyświetli alert
? Czy pokaże “Powiedziałeś/aś Cześć do Alice”? A może “Powiedziałeś/aś Cześć do Boba”? Spróbuj zgadnąć na podstawie tego, czego udało się ci dowiedzieć, a następnie sprawdź:
import { useState } from 'react'; export default function Form() { const [to, setTo] = useState('Alice'); const [message, setMessage] = useState('Cześć'); function handleSubmit(e) { e.preventDefault(); setTimeout(() => { alert(`Powiedziałeś/aś ${message} do ${to}`); }, 5000); } return ( <form onSubmit={handleSubmit}> <label> Do:{' '} <select value={to} onChange={e => setTo(e.target.value)}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> </select> </label> <textarea placeholder="Wiadomość" value={message} onChange={e => setMessage(e.target.value)} /> <button type="submit">Wyślij</button> </form> ); }
React zachowuje wartości stanu jako “utrwalone” w procedurach obsługi zdarzeń dla danego renderowania. Nie musisz się martwić, czy stan zmienił się podczas wykonywania kodu.
A co, jeśli chcesz odczytać najnowszy stan przed ponownym renderowaniem? Wtedy będziesz potrzebować funkcji aktualizującej stan, którą omówimy na następnej stronie!
Powtórka
- Ustawienie stanu żąda nowego renderowania.
- React przechowuje stan poza komponentem, jakby na półce.
- Kiedy wywołujesz
useState
, React daje ci migawkę stanu dla tego renderowania. - Zmienne i procedury obsługi zdarzeń nie są w stanie “przetrwać” ponownego renderowania. Każde renderowanie ma własne procedury obsługi.
- Każde renderowanie (i funkcje wewnątrz niego) zawsze “widzą” migawkę stanu, którą React przekazał temu renderowaniu.
- Możesz w myślach podstawiać stan w procedurach obsługi zdarzeń, podobnie jak myślisz o wyrenderowanym JSX.
- Procedury obsługi zdarzeń utworzone w przeszłości mają wartości stanu z renderowania, w którym zostały utworzone.
Wyzwanie 1 z 1: Zaimplementuj światła uliczne
Oto komponent świateł na przejściu dla pieszych, który przełącza się po naciśnięciu przycisku:
import { useState } from 'react'; export default function TrafficLight() { const [walk, setWalk] = useState(true); function handleClick() { setWalk(!walk); } return ( <> <button onClick={handleClick}> Zmień na {walk ? 'Stój' : 'Idź'} </button> <h1 style={{ color: walk ? 'darkgreen' : 'darkred' }}> {walk ? 'Idź' : 'Stój'} </h1> </> ); }
Dodaj alert
do procedury obsługi kliknięcia. Kiedy światło jest zielone i wyświetla “Idź”, kliknięcie przycisku powinno pokazać “Następnie będzie Stój”. Kiedy światło jest czerwone i wyświetla “Stój”, kliknięcie przycisku powinno pokazać “Następnie będzie Idź”.
Czy ma znaczenie, czy umieścisz alert
przed czy po wywołaniu setWalk
?