Tilan säilyttäminen ja nollaus
Tila on eristetty komponenttien välillä. React pitää kirjaa siitä, mikä tila kuuluu mihinkin komponenttiin sen perusteella, mikä on niiden paikka käyttöliittymäpuussa. Voit hallita, milloin tila säilytetään ja milloin se nollataan uudelleen renderöintiä varten.
Tulet oppimaan
- Miten React “näkee” komponentin rakenteen
- Milloin React päättää säilyttää tai nollata tilan
- Miten pakottaa React nollaamaan komponentin tila
- Miten avaimet ja tyypit vaikuttavat tilan säilyttämiseen
Käyttöliittymäpuu
Selaimet käyttävät monia puumalleja käyttöliittymän mallintamiseen. DOM edustaa HTML-elementtejä, CSSOM tekee saman CSS:lle. On olemassa jopa saavutettavuuspuu!
React käyttää myös puurakenteita käyttöliittymäsi hallintaan ja mallintamiseen. React rakentaa UI puita JSX koodistasi. Sitten React DOM päivittää selaimen DOM elementit vastaamaan tuota UI puuta. (React Native kääntää nämä puut näkymiksi, jotka voidaan näyttää puhelinalustoilla.)
Tila on sidottu sijaintiin puussa
Kun annat komponentille tilan, saatat ajatella, että tila “asuu” komponentin sisällä. Mutta tila oikeasti pidetään Reactin sisällä. React yhdistää jokaisen hallussa olevan tilatiedon oikeaan komponenttiin sen mukaan, missä kohtaa käyttöliittymäpuuta kyseinen komponentti sijaitsee.
Tässä esimerkissä on vain yksi <Counter />
JSX tagi, mutta se on renderöity kahdessa eri kohdassa:
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)}> Add one </button> </div> ); }
Miltä tämä näyttäisi puuna:
Nämä ovat kaksi erillistä laskuria, koska ne on renderöity niiden omissa sijainneissa puussa. Sinun ei tarvitse muistaa näitä sijainteja käyttääksesi Reactia, mutta voi olla hyödyllistä ymmärtää miksi se toimii.
Reactissa, kukin komponentti ruudulla omaa eristetyn tilan. Esimerkiksi, jos renderöiti kaksi Counter
komponenttia vierekkäin, molemmilla on niiden omat, itsenäiset, score
ja hover
tilat.
Kokeile klikata molempia laskureita ja huomaat, etteivät ne vaikuta toisiinsa:
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)}> Add one </button> </div> ); }
Kuten voit nähdä, kun toista laskuria päivitetään, vain sen komponentin tila päivittyy:
React pitää tilan muistissa niin kauan kuin renderlit samaa komponenttia samassa sijainnissa. Tämän nähdäksesi, korota molempia laskureita ja sitten poista toinen komponentti poistamalla valinta “Render the second counter” valintaruudusta, ja sitten lisää se takaisin valitsemalla se uudelleen:
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) }} /> Render the second counter </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)}> Add one </button> </div> ); }
Huomaa, kun lopetat toisen laskurin renderöimisen, sen tila katoaa täysin. Tämä tapahtuu koska kun React poistaa komponentin, se poistaa sen tilan.
Kun valitset “Render the second counter”, toinen Counter
ja sen tula alustetaan tyjästä (score = 0
) ja lisätään DOM:iin.
React säilyttää komponentin tilan niin kauan kuin se on renderöity sen sijainnissa käyttöliittymäpuussa. Jos se poistetaan tai toinen komponentti renderöidään sen sijaintiin, React hävittää sen tilan.
Sama komponentti samassa sijainnissa säilyttää tilan
Tässä esimerkissä on kaksi eri <Counter />
tagia:
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) }} /> Use fancy styling </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)}> Add one </button> </div> ); }
Kun valitset tai tyhjäät valintaruudun, laskurin tila ei poistu. Riippuen onko isFancy
true
vai false
, <Counter />
on aina ensimmäinen App
komponentin palauttaman div
:n lapsi:
Se on sama komponentti samassa sijainnissa, joten Reactin näkökulmasta se on sama laskuri.
Eri komponentit samassa sijainnissa nollaavat tilan
Tässä esimerkissä valintaruudun valitseminen korvaa <Counter>
:n <p>
tagilla:
import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return ( <div> {isPaused ? ( <p>See you later!</p> ) : ( <Counter /> )} <label> <input type="checkbox" checked={isPaused} onChange={e => { setIsPaused(e.target.checked) }} /> Take a break </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)}> Add one </button> </div> ); }
Tässä vaihdat eri komponenttityyppejä samassa sijainnissa. Aluksi <div>
:n ensimmäinen lapsi oli Counter
. Mutta kun vaihdat <p>
:ksi, React poistaa Counter
n UI puusta ja tuhoaa sen tilan.
Huomaathan, kun renderöit eri komponentin samassa sijainnissa, se nollaa sen koko alipuun tilan. Nähdäksesi, miten tämä toimii, lisää laskuri ja valitse sitten valintaruutu:
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) }} /> Use fancy styling </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)}> Add one </button> </div> ); }
Laskurin tila nollautuu kun valintaruutu valitaan. Vaikka renderöit Counter
:n, ensimmäinen <div>
:n lapsi muuttuu <div>
:stä <section>
:ksi. Kun div
:n lapsi poistettiin DOM:sta, sen koko alipuu (mukaan lukien Counter
ja sen tila) tuhottiin.
Nyrkkisääntönä, jos haluat säilyttää tilan renderöintien välillä, puun rakenteen on oltava “sama” renderöinnistä toiseen. Jos rakenteet ovat erilaiset, tila tuhotaan, sillä React tuhoaa tilan, kun se poistaa komponentin puusta.
Tilan nollaaminen samassa sijainnissa
Oletuksena React säilyttää komponentin tilan, kun se pysyy samassa sijainnissa. Yleensä tämä on juuri sitä, mitä haluat, joten se on järkevää oletusarvoa. Mutta joskus haluat nollata komponentin tilan. Harkitse tätä sovellusta, joka antaa kahdelle pelaajalle mahdollisuuden pitää kirjaa heidän pisteistään jokaisella vuorollaan:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter person="Tommi" /> ) : ( <Counter person="Sara" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Seuraava pelaaja! </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>{person}n pisteet: {score}</h1> <button onClick={() => setScore(score + 1)}> Lisää yksi </button> </div> ); }
Tällä hetkellä, kun vaihdat pelaajaa, tila säilyy. Kaksi Counter
-komponenttia näkyy samassa sijainnissa, joten React näkee ne samana Counter
-komponenttina, jonka person
-prop on muuttunut.
Mutta tässä sovelluksessa niiden tulisi olla eri laskureita. Ne saattavat näkyä samassa kohdassa käyttöliittymää, mutta toinen laskuri on Tommin ja toinen laskuri Saran.
On kaksi tapaa nollata tila kun vaihdetaan niiden välillä:
- Renderöi komponentit eri sijainneissa
- Anna kullekin komponentille eksplisiittinen identiteetty
key
propilla
1. Vaihtoehto: Komponentin renderöiminen eri sijainneissa
Jos haluat, että nämä kaksi Counter
-komponenttia ovat erillisiä, voit renderöidä ne eri sijainneissa:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA && <Counter person="Tommi" /> } {!isPlayerA && <Counter person="Sara" /> } <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Seuraava pelaaja! </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>{person}n pisteet: {score}</h1> <button onClick={() => setScore(score + 1)}> Lisää yksi </button> </div> ); }
- Aluksi,
isPlayerA
on arvoltaantrue
. Joten ensimmäinen sijainti sisältääCounter
tilan ja toinen on tyhjä. - Kun klikkaat “Seuraava pelaaja” painiketta, ensimmäinen sijainti tyhjenee ja seuraava sisältää
Counter
:n.
Kunkin Counter
:n tila tuhotaan joka kerta kun se poistetaan DOM:sta. Tämän takia ne nollautuvat joka kerta kun painat painiketta.
Tämä ratkaisu on kätevä, kun sinulla on vain muutamia riippumattomia komponentteja, jotka renderöidään samassa paikassa. Tässä esimerkissä sinulla on vain kaksi, joten ei ole hankalaa renderöidä molemmat erikseen JSX:ssä.
2. Vaihtoehto: Tilan nollaaminen avaimella
On myös toinen, yleisempi tapa, jolla voit nollata komponentin tilan.
Olet saattanut nähdä key
propseja kun renderöitiin listoja. Avaimet eivät ole vain listoille! Voit käyttää avaimia saadaksesi Reactin tunnistamaan erot komponenttien välillä. Oletuksena, React käyttää järjestystä (“ensimmäinen laskuri”, “toinen laskuri”) erottaakseen komponentit toisistaan. Mutta avaimilla voit kertoa Reactille, että tämä ei ole vain ensimmäinen laskuri ,tai toinen laskuri, vaan tarkemmin—esimerkiksi Tommin laskuri. Näin React tietää Tommin laskurin joka kerta kun näkee sen puussa!
Tässä esimerkissä, kaksi <Counter />
:a eivät jaa tilaa vaikka ne näyttävät samassa paikassa JSX:ssä:
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); }}> Next player! </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>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Tila säily vaikka Tommin ja Saran välillä vaihdetaan. Tämä tapahtuu koska annoit niille eri key
:t.
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
Sen sijaan että komponentin sijainti tulisi järjestyksestä pääkomponentissa, key
:n määrittäminen kertoo Reactille, että key
itsessään on osa sijaintia. Tämän takia vaikka renderöit ne samassa sijainnissa JSX:ssä, Reactin perspektiivistä nämä ovat kaksi eri laskuria. Seurauksena, ne eivät koskaan jaa tilaa. Joka kerta kun laskuri näkyy ruudulla, sen tila on luotu. Joka kerta kun poistetaan, sen tila tuhotaan. Näiden välillä vaihtelu nollaa ja tuhoaa niiden tilan uudelleen ja uudelleen.
Lomakkeen nollaaminen avaimella
Tilan nollaaminen on erittäin hyödyllistä kun käsitellään lomakkeita.
Tässä chat-sovelluksessa, <Chat>
komponentti sisältää tilan tekstisyötteelle:
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' } ];
Kokeile syöttää jotain kenttään ja painaa “Alice” tai “Bob” valitaksesi eri vastaanottajan. Huomaat, että syöte säilyy, koska <Chat>
renderöidään samassa sijainnissa puussa.
Monissa sovelluksisa, tämä saattaa olla haluttu ominaisuus, mutta ei chat-sovelluksessa! Et halua käyttäjän lähettävän kirjoitettua viestiä väärälle henkilölle vanhingollisen klikkauksen seurauksena. Korjataksesi tämän, lisää key
:
<Chat key={to.id} contact={to} />
Tämä varmistaa, että kun valitset eri vastaanottajan, Chat
komponentti tullaan luomaan alusta, mukaan lukien tila puussa sen alla. React tulee myös luomaan uudelleen DOM elementit niiden uudelleenkäytön sijaan.
Nyt vaihtaminen vastaanottajien välillä tyhjää tekstisyötteen:
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' } ];
Syväsukellus
Oikeassa chat-sovelluksessa, haluaisit varmaankin palauttaa syötteen kun käyttäjä valitsee edellisen vastaanottajan uudestaan. On useita tapoja pitää tila “hengissä” komponentille, joka ei ole enää näkyvissä:
- Voit renderöidä kaikki chatit yhden sijaan, ja piilottaa loput CSS:llä. Keskusteluja ei tultaisi poistamaan puusta, joten niiden paikallinen tila säilyisi. Tämä tapa toimii yksinkertaisille käyttöliittymille, mutta se voi koitua erittäin hitaaksi, jos piilotetut puut ovat suuria ja sisältävät paljon DOM nodeja.
- Voit nostaa tilan ylös ja pitää viestiluonnokset jokaiselle vastaanottajalle pääkomponentissa. Tällä tavalla, kun lapsikomponentti poistetaan, se ei haittaa, sillä pääkomponentti pitää yllä tärkeät tiedot. Tämä on yleisin ratkaisu.
- Saatat myös käyttää eri lähdettä Reactin tilan lisäksi. Esimerkiksi, saatat haluta viestiluonnoksen säilyvän vaikka käyttäjä vahingossa sulkisi sivun. Tämän tehdäksesi, voisit asettaa
Chat
komponentin alustamaan tilan lukemallalocalStorage
:a ja tallentamaan myös luonnokset sinne.
Riippumatta siitä minkä strategian valitset, chatti Alicen kanssa on havainnollisesti eri kuin Bobin kanssa, joten on järkevää antaa <Chat>
komponentille key
propsi, joka pohjautuu nykyiseen vastaanottajaan.
Kertaus
- React pitää tilan niin pitkään kuin sama komponentti on renderöity samassa sijainnissa.
- Tilaa ei pidetä JSX tageissa. Se liittyy puun sijaintiin, johon laitat JSX:n.
- Voit pakottaa alipuun nollaamaan tilansa antamalla sille toisen avaimen.
- Älä sijoita komponenttimäärityksiä toistensa sisään, tai nollaat tilan vahingossa.
Haaste 1 / 5: Korjaa katoava syöttöteksti
Tämä esimerkki näyttää vistin kun painat painiketta. Kuitenkin painikkeen painaminen vahingossa nollaa syötteen. Miksi näin tapahtuu? Korjaa se, jotta painikkeen painaminen ei nollaa tekstisyötettä.
import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return ( <div> <p><i>Hint: Your favorite city?</i></p> <Form /> <button onClick={() => { setShowHint(false); }}>Hide hint</button> </div> ); } return ( <div> <Form /> <button onClick={() => { setShowHint(true); }}>Show hint</button> </div> ); } function Form() { const [text, setText] = useState(''); return ( <textarea value={text} onChange={e => setText(e.target.value)} /> ); }