Zachowywanie i resetowanie stanu
Stan jest izolowany między komponentami. React śledzi, który stan należy do którego komponentu na podstawie ich miejsca w drzewie interfejsu użytkownika. Możesz kontrolować, kiedy zachować stan, a kiedy go zresetować między przerenderowaniami.
W tej sekcji dowiesz się
- Kiedy React decyduje się, aby zachować lub zresetować stan
- Jak zmusić React do zresetowania stanu komponentu
- Jak klucze i typy wpływają na to, czy stan jest zachowany
Stan jest powiązany z pozycją w drzewie renderowania
React buduje drzewa renderowania dla struktury komponentów w twoim interfejsie użytkownika.
Kiedy dodajesz stan do komponentu, możesz myśleć, że stan “żyje” wewnątrz komponentu. Jednak w rzeczywistości jest on przechowywany wewnątrz Reacta. Kojarzy on każdą część stanu, którą przechowuje, z odpowiednim komponentem na podstawie jego miejsca w drzewie renderowania.
W poniższym przykładzie, jest tylko jeden tag <Counter />
w składni JSX, ale jest renderowany na dwóch różnych pozycjach:
import { useState } from 'react'; export default function App() { const counter = <Counter />; return ( <div> {counter} {counter} </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Dodaj jeden </button> </div> ); }
Oto jak te komponenty wyglądają jako drzewo:
To są dwa oddzielne liczniki, ponieważ każdy jest renderowany na swojej własnej pozycji w drzewie. Zazwyczaj nie musisz myśleć o tych pozycjach, aby korzystać z Reacta, ale zrozumienie, jak to działa, może być przydatne.
W Reakcie, każdy komponent na ekranie ma całkowicie izolowany stan. Na przykład, jeśli renderujesz dwa komponenty Counter
obok siebie, każdy z nich będzie miał swoje własne, niezależne stany score
i hover
.
Spróbuj klikać oba liczniki i zauważ, że nie wpływają one na siebie nawzajem:
import { useState } from 'react'; export default function App() { return ( <div> <Counter /> <Counter /> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Dodaj jeden </button> </div> ); }
Jak widać, gdy jeden licznik jest aktualizowany, tylko stan dla tego komponentu jest aktualizowany:
React będzie przechowywać stan tak długo, jak długo renderujesz ten sam komponent na tej samej pozycji w drzewie. Aby to zaobserwować, zwiększ oba liczniki, a następnie usuń drugi komponent, odznaczając pole wyboru “Renderuj drugi licznik”, a potem dodaj go z powrotem, zaznaczając je ponownie:
import { useState } from 'react'; export default function App() { const [showB, setShowB] = useState(true); return ( <div> <Counter /> {showB && <Counter />} <label> <input type="checkbox" checked={showB} onChange={e => { setShowB(e.target.checked) }} /> Renderuj drugi licznik </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Dodaj jeden </button> </div> ); }
Zauważ, że w momencie, gdy przestajesz renderować drugi licznik, jego stan znika całkowicie. Dzieje się tak, ponieważ kiedy React usuwa komponent, niszczy też jego stan.
Kiedy zaznaczasz pole wyboru “Renderuj drugi licznik”, drugi komponent Counter
i jego stan są inicjalizowane od nowa (score = 0
) i dodawane do drzewa DOM.
React zachowuje stan komponentu tak długo, jak jest on renderowany na swojej pozycji w drzewie UI. Jeśli zostanie on usunięty lub na jego miejsce zostanie wyrenderowany inny komponent, React odrzuci jego stan.
Ten sam komponent na tej samej pozycji zachowuje stan
W tym przykładzie znajdują się dwa różne tagi <Counter />
:
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <Counter isFancy={true} /> ) : ( <Counter isFancy={false} /> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Użyj wyszukanego stylu </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Dodaj jeden </button> </div> ); }
Gdy zaznaczysz lub odznaczysz pole wyboru, stan licznika nie jest resetowany. Niezależnie od tego, czy isFancy
jest ustawione na true
, czy false
, komponent <Counter />
jest zawsze pierwszym potomkiem elementu div
zwracanego z głównego komponentu App
To ten sam komponent na tej samej pozycji, więc z perspektywy Reacta to jest ten sam licznik.
Różne komponenty na tej samej pozycji resetują stan
W tym przykładzie zaznaczenie pola wyboru zastąpi komponent <Counter>
elementem <p>
:
import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return ( <div> {isPaused ? ( <p>Do zobaczenia później!</p> ) : ( <Counter /> )} <label> <input type="checkbox" checked={isPaused} onChange={e => { setIsPaused(e.target.checked) }} /> Zrób przerwę </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Dodaj jeden </button> </div> ); }
Tutaj przełączasz się między różnymi typami komponentów na tej samej pozycji. Początkowo, pierwszy potomek <div>
zawierał komponent Counter
. Jednak kiedy zamieniono go na p
, React usunął komponent Counter
z drzewa UI i zniszczył jego stan.
Również renderowanie innego komponentu na tej samej pozycji, resetuje stan całego jego poddrzewa. Aby zobaczyć, jak to działa, zwiększ licznik, a następnie zaznacz pole wyboru:
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <div> <Counter isFancy={true} /> </div> ) : ( <section> <Counter isFancy={false} /> </section> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Użyj wyszukanego stylu </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Dodaj jeden </button> </div> ); }
Stan licznika zostaje zresetowany, gdy klikniesz pole wyboru. Chociaż renderujesz komponent Counter
, pierwszy potomek elementu div
zmienia się z div
na section
. Kiedy potomek div
został usunięty z drzewa DOM, całe drzewo poniżej niego (w tym komponent Counter
i jego stan) zostało również zniszczone.
Ogólna zasada jest taka, że jeśli chcesz zachować stan pomiędzy przerenderowaniami, struktura drzewa musi “pasować” między jednym a drugim renderowaniem. Jeśli struktura jest inna, stan zostaje zniszczony, ponieważ React usuwa stan, gdy usuwa komponent z drzewa.
Resetowanie stanu na tej samej pozycji
Domyślnie React zachowuje stan komponentu, dopóki pozostaje on na tej samej pozycji. Zazwyczaj jest to dokładnie to, czego oczekujesz, więc to domyślne zachowanie ma sens. Czasami jednak możesz chcieć zresetować stan komponentu. Rozważ poniższą aplikację, która pozwala dwóm graczom śledzić swoje wyniki podczas każdej tury:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter person="Taylor" /> ) : ( <Counter person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Następny gracz! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>Wynik gracza {person}: {score}</h1> <button onClick={() => setScore(score + 1)}> Dodaj jeden </button> </div> ); }
Obecnie, gdy zmieniasz gracza, wynik jest zachowany. Dwa komponenty licznika Counter
pojawiają się na tej samej pozycji, więc React widzi je jako ten sam komponent Counter
, w którym zmieniła się właściwość person
.
Ale w tym przypadku, koncepcyjnie powinny to być dwa osobne liczniki. Mogą pojawiać się w tym samym miejscu w interfejsie użytkownika, ale jeden z nich to licznik dla Taylora, a drugi dla Sarah.
Istnieją dwa sposoby na zresetowanie stanu podczas przełączania się między licznikami:
- Renderowanie komponentów na różnych pozycjach
- Nadanie każdemu komponentowi konkretnej tożsamości za pomocą klucza
key
Opcja 1: Renderowanie komponentu na różnych pozycjach
Jeśli chcesz, aby te dwa komponenty Counter
były niezależne, możesz wyrenderować je na dwóch różnych pozycjach:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA && <Counter person="Taylor" /> } {!isPlayerA && <Counter person="Sarah" /> } <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Następny gracz! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>Wynik gracza {person}: {score}</h1> <button onClick={() => setScore(score + 1)}> Dodaj jeden </button> </div> ); }
- Początkowo,
isPlayerA
ma wartośćtrue
. W związku z tym pierwsza pozycja zawiera komponentCounter
, a druga jest pusta. - Kiedy klikniesz przycisk “Następny gracz”, pierwsza pozycja się opróżnia, a w drugiej pojawia się komponent
Counter
.
Stan każdego komponentu Counter
jest niszczony za każdym razem, gdy jest usuwany z drzewa DOM. To dlatego stany te resetują się za każdym razem, gdy klikniesz przycisk.
To rozwiązanie jest wygodne, gdy masz tylko kilka niezależnych komponentów renderowanych w tym samym miejscu. W tym przykładzie są tylko dwa, więc nie ma problemu z renderowaniem obu osobno w składni JSX.
Opcja 2: Resetowanie stanu za pomocą klucza (ang. key)
Istnieje także inny, bardziej ogólny sposób na zresetowanie stanu komponentu.
Przy renderowaniu list można zauważyć użycie kluczy. Klucze te nie są tylko dla list! Możesz użyć kluczy, aby pomóc Reactowi rozróżnić dowolne komponenty. Domyślnie React używa kolejności w obrębie rodzica (“pierwszy licznik”, “drugi licznik”), aby odróżnić komponenty. Jednak klucze pozwalają powiedzieć Reactowi, że to nie jest tylko pierwszy licznik czy drugi licznik, ale jakiś konkretny licznik - na przykład licznik Taylora. W ten sposób React będzie wiedział., że to licznik Taylora, niezależnie od tego, gdzie pojawi się on w drzewie!
W tym przykładzie dwa komponenty <Counter />
nie dzielą stanu, mimo że pojawiają się w tym samym miejscu w składni JSX:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter key="Taylor" person="Taylor" /> ) : ( <Counter key="Sarah" person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Następny gracz! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>Wynika gracza {person}: {score}</h1> <button onClick={() => setScore(score + 1)}> Dodaj jeden </button> </div> ); }
Przełączanie między graczami Taylor a Sarah nie zachowuje ich stanu. Dzieje się tak, ponieważ przypisano im różne klucze key
:
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
Określenie klucza key
mówi Reactowi, aby użył jego samego jako części pozycji, zamiast polegać na kolejności w obrębie rodzica. Dlatego, chociaż renderujesz je w tym samym miejscu w składni JSX, React postrzega je jako dwa różne liczniki, więc nigdy nie będą one dzielić stanu. Za każdym razem, gdy licznik pojawia się na ekranie, jego stan jest tworzony. Za każdym razem, gdy jest usuwany, jego stan jest niszczony. Przełączanie między komponentami resetuje ich stan w kółko.
Resetowanie formularza za pomocą klucza
Resetowanie stanu za pomocą klucza jest szczególnie przydatne podczas pracy z formularzami.
W poniższej aplikacji czatu, komponent <Chat>
zawiera stan dla pola tekstowego:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
Spróbuj wpisać coś w polu tekstowym, a następnie wybierz innego odbiorcę, klikając „Alice” lub „Bob”. Zauważ, że stan pola tekstowego jest zachowywany, ponieważ komponent <Chat>
jest renderowany na tej samej pozycji w drzewie komponentów.
W wielu aplikacjach może to być pożądane zachowanie, ale nie w aplikacji czatu! Nie chcesz, aby użytkownik wysłał wiadomość do niewłaściwej osoby z powodu przypadkowego kliknięcia. Aby to naprawić, dodaj klucz key
:
<Chat key={to.id} contact={to} />
To rozwiązanie zapewnia, że kiedy wybierzesz innego odbiorcę, komponent Chat
zostanie stworzony od nowa, łącznie z całym stanem w drzewie poniżej niego. React również ponownie utworzy elementy drzewa DOM zamiast ponownie ich użyć.
Teraz zmiana odbiorcy zawsze wyczyści pole tekstowe:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.id} contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
Dla dociekliwych
W prawdziwej aplikacji czatu, prawdopodobnie chcesz odzyskać stan wprowadzonego tekstu, gdy użytkownik ponownie wybierze poprzedniego odbiorcę. Istnieje kilka sposobów na to, aby stan komponentu, który nie jest już widoczny, pozostał zachowany:
- Możesz renderować wszystkie czaty zamiast tylko aktualnie wybranego i ukryć pozostałe za pomocą stylów CSS. Czat nie zostanie usunięty z drzewa, więc jego lokalny stan będzie zachowany. To rozwiązanie działa świetnie dla prostych interfejsów, ale może stać się powolne, jeśli ukryte drzewa będą duże i będą zawierały dużo węzłów drzewa DOM.
- Możesz przenieść stan wyżej i przechowywać wiadomości oczekujące dla każdego odbiorcy w komponencie nadrzędnym. Dzięki temu, nawet gdy komponenty podrzędne zostaną usunięte, nie ma to znaczenia, ponieważ to rodzic przechowuje istotne informacje. Jest to najczęściej stosowane rozwiązanie.
- Możesz również użyć innego sposobu zamiast stanu Reacta. Na przykład, być może chcesz, aby wersja robocza wiadomości przetrwała nawet w przypadku omyłkowego zamknięcia strony przez użytkownika. Aby to zaimplementować, komponent
Chat
mógłby inicjalizować swój stan, odczytując go zlocalStorage
, a także zapisywać tam wersje robocze wiadomości.
Bez względu na to, którą strategię wybierzesz, czat z Alicją jest koncepcyjnie inny niż czat z Bobem, więc sensowne jest przypisanie klucza key
do drzewa <Chat>
na podstawie aktualnego odbiorcy.
Powtórka
- React utrzymuje stan tak długo, jak ten sam komponent jest renderowany w tym samym miejscu.
- Stan nie jest przechowywany w znacznikach JSX. Jest on powiązany z pozycją drzewa, w której umieszczasz ten kod JSX.
- Możesz wymusić zresetowanie stanu poddrzewa, nadając mu inny klucz
key
. - Nie zagnieżdżaj definicji komponentów, ponieważ przypadkowo zresetujesz stan.
Wyzwanie 1 z 5: Napraw znikający tekst w polu input
W poniższym przykładzie po naciśnięciu przycisku wyświetlana jest wiadomość. Jednak przypadkowe naciśnięcie przycisku resetuje również pole wprowadzania tekstu. Dlaczego tak się dzieje? Napraw to, aby naciśnięcie przycisku nie resetowało tekstu w polu wprowadzania.
import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return ( <div> <p><i>Podpowiedź: Twoje ulubione miasto?</i></p> <Form /> <button onClick={() => { setShowHint(false); }}>Ukryj podpowiedź</button> </div> ); } return ( <div> <Form /> <button onClick={() => { setShowHint(true); }}>Pokaż podpowiedź</button> </div> ); } function Form() { const [text, setText] = useState(''); return ( <textarea value={text} onChange={e => setText(e.target.value)} /> ); }