Čuvanje i resetovanje state-a
State je izolovan između komponenata. React prati koji state pripada kojoj komponenti na osnovu njenog mesta u UI stablu. Možete kontrolisati kada čuvati state, a kada ga resetovati između ponovnih rendera.
Naučićete:
- Kada React bira da li da čuva ili resetuje state
- Kako naterati React da resetuje state komponente
- Kako ključevi i tipovi utiču na to da li je state sačuvan
State je povezan sa pozicijom u stablu renderovanja
React pravi stabla renderovanja za strukturu komponenti na vašem UI-u.
Kada komponenti date state, možete pomisliti da state “živi” unutar komponente. Ali, state se zapravo čuva unutar React-a. React povezuje svaki deo state-a sa tačnom komponentom na osnovu mesta na kom se ta komponenta nalazi u stablu renderovanja.
Ovde postoji samo jedan <Counter />
JSX tag, ali je renderovan na dve različite pozicije:
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 jedan </button> </div> ); }
Evo kako to izgleda u obliku stabla:


React stablo
Ovo su dva odvojena brojača jer je svaki renderovan na svojoj poziciji u stablu. Obično ne morate razmišljati o ovim pozicijama da biste koristili React, ali može biti korisno da razumete kako to funkcioniše.
U React-u, svaka komponenta na ekranu ima potpuno izolovan state. Na primer, ako renderujete dve Counter
komponente jednu pored druge, svaka će imati svoje nezavisne score
i hover
state-ove.
Probajte da kliknete oba brojača i primetite da ne utiču jedan na drugog:
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 jedan </button> </div> ); }
Kao što možete videti, kada je jedan brojač ažuriran, jedino se state za tu komponentu ažurira:


Ažuriranje state-a
React će držati state dok god istu komponentu renderujete na istoj poziciji u stablu. Da biste ovo primetili, inkrementirajte oba brojača, uklonite drugu komponentu tako što ćete odštiklirati checkbox “Renderuj drugi brojač”, a onda je štikliranjem dodajte ponovo:
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 brojač </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 jedan </button> </div> ); }
Primetite da u trenutku kada prestanete da renderujete drugi brojač, njegov state potpuno nestaje. To je zato što kad React ukloni komponentu, uklanja i njen state.


Brisanje komponente
Kada štiklirate “Renderuj drugi brojač”, drugi Counter
i njegov state se inicijalizuju od nule (score = 0
) i dodaju u DOM.


Dodavanje komponente
React čuva state komponente dok god se renderuje na svojoj poziciji u UI stablu. Ako se ukloni, ili se druga komponenta renderuje na istoj poziciji, React odbacuje njen state.
Ista komponenta na istoj poziciji čuva state
U ovom primeru postoje dva različita <Counter />
tag-a:
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) }} /> Koristi fensi stajling </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 jedan </button> </div> ); }
Kada promenite checkbox, counter state ne bude resetovan. Nezavisno od toga da li je isFancy
jednak true
ili false
, uvek ćete imati <Counter />
kao prvo dete div
-a koje je vraćeno iz korenske App
komponente:


Ažuriranje App
state-a ne resetuje Counter
zato što Counter
ostaje na istoj poziciji
To je ista komponenta na istoj poziciji, pa je iz React perspektive to isti brojač.
Različite komponente na istoj poziciji resetuju state
U ovom primeru, štikliranje checkbox-a će zameniti <Counter>
sa <p>
:
import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return ( <div> {isPaused ? ( <p>Vidimo se posle!</p> ) : ( <Counter /> )} <label> <input type="checkbox" checked={isPaused} onChange={e => { setIsPaused(e.target.checked) }} /> Uzmi pauzu </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 jedan </button> </div> ); }
Ovde menjate između različitih tipova komponenti na istoj poziciji. Inicijalno, prvo dete <div>
-a je bio Counter
. Ali kad ste ga zamenili sa p
, React je uklonio Counter
iz UI stabla i uništio njegov state.


Kada se Counter
zameni sa p
, Counter
je obrisan i p
je dodat


Kada se ponovo promene, p
je obrisan, a Counter
je dodat
Takođe, kada renderujete različitu komponentu na istoj poziciji, resetuje se state od čitavog podstabla. Da vidite kako ovo radi, inkrementirajte brojač i onda štiklirajte checkbox:
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) }} /> Koristi fensi stajling </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 jedan </button> </div> ); }
State Counter
-a se resetuje kada kliknete na checkbox. Iako renderujete Counter
, prvo dete div
-a se promeni sa section
na div
. Kada je dečji section
uklonjen iz DOM-a, celokupno stablo ispod (uključujući Counter
i njegov state) je takođe uništeno.


Kada se section
promeni u div
, section
je obrisan, a novi div
je dodat


Kada se ponovo promene, div
je obrisan, a novi section
je dodat
Kao pravilo, ako želite da sačuvate state između ponovnih rendera, struktura vašeg stabla mora da se “poklapa” od jednog do drugog rendera. Ako je struktura različita, state će biti uništen jer React uništava state kada uklanja komponentu iz stabla.
Resetovanje state-a na istoj poziciji
Po default-u, React čuva state komponente dok god ostaje na istoj poziciji. Uglavnom je to upravo ono što želite, pa ima smisla da je to default ponašanje. Ali, ponekad možete želeti da resetujete state komponente. Razmotrite ovu aplikaciju koja omogućava dvojici igrača da prate svoje poene tokom svakog poteza:
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); }}> Naredni igrač! </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>Poeni za osobu {person}: {score}</h1> <button onClick={() => setScore(score + 1)}> Dodaj jedan </button> </div> ); }
Trenutno, kada promenite igrača, poeni ostaju sačuvani. Dve Counter
komponente se nalaze na istoj poziciji, pa ih React posmatra kao isti Counter
čiji se person
prop promenio.
Ali konceptualno, u ovoj aplikaciji to trebaju biti dva odvojena brojača. Iako se pojavljuju na istom mestu na UI-u, jedan brojač je za Taylor, a drugi za Sarah.
Postoje dva načina da resetujete state kada ih menjate:
- Renderujte komponente na različitim pozicijama
- Dajte svakoj komponenti eksplicitni identitet sa
key
Opcija 1: Renderovanje komponente na različitim pozicijama
Ako želite da ova dva Counter
-a budu nezavisna, možete ih renderovati na dve različite pozicije:
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); }}> Naredni igrač! </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>Poeni za osobu {person}: {score}</h1> <button onClick={() => setScore(score + 1)}> Dodaj jedan </button> </div> ); }
- Inicijalno,
isPlayerA
jetrue
. To znači da prva pozicija sadržiCounter
state, a druga je prazna. - Kada kliknete dugme “Naredni igrač” prva pozicija se briše, a druga sada sadrži
Counter
.


Inicijalni state


Kliknuto “naredno”


Kliknuto “naredno” opet
State svakog Counter
-a bude uništen svaki put kad je uklonjen iz DOM-a. Zbog toga se resetuju svaki put kada kliknete dugme.
Ovo rešenje je zgodno kada imate samo par komponenti renderovanih na istom mestu. U ovom primeru, imate ih samo dve, pa nije problem renderovati ih u različitom JSX-u.
Opcija 2: Resetovanje state-a uz key
Postoji i drugi, više generički, način da resetujete state komponente.
Možda ste videli key
-eve tokom renderovanja listi. Ključevi nisu samo za liste! Možete koristiti ključeve da naterate React da razlikuje bilo koje komponente. Po default-u, React koristi redosled unutar roditelja (“prvi brojač”, “drugi brojač”) da razlikuje komponente. Ali, ključevi vam omogućavaju da kažete React-u da to nije samo prvi brojač, ili drugi brojač, već poseban brojač—na primer, Taylor-ov brojač. Na ovaj način, React će znati za Taylor-ov brojač gde god da se pojavi u stablu!
U ovom primeru, dva <Counter />
-a ne dele state iako se nalaze na istom mestu u JSX-u:
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); }}> Naredni igrač! </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>Poeni za osobu {person}: {score}</h1> <button onClick={() => setScore(score + 1)}> Dodaj jedan </button> </div> ); }
Menjanje između Taylor i Sarah ne čuva state. To je zato što ste im dali različite key
-eve:
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
Specificiranje key
-a govori React-u da koristi key
kao deo pozicije umesto rasporeda unutar roditelja. Zbog toga, iako ih renderujete na istom mestu u JSX-u, React ih vidi kao dva različita brojača, tako da nikad neće deliti state. Svaki put kad se brojač pojavi na ekranu, njegov state je kreiran. Svaki put kad je uklonjen, njegov state je uništen. Promena između njih resetuje njihov state iznova.
Resetovanje forme sa key-em
Resetovanje state-a uz key je posebno korisno kada radite sa formama.
U ovoj aplikaciji za poruke, <Chat>
komponenta sadrži state za tekstualni input:
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' } ];
Pokušajte da unesete nešto u input, a onda pritisnite “Alice” ili “Bob” da odaberete drugog primaoca. Primetićete da je input state sačuvan zato što je <Chat>
renderovan na istoj poziciji u stablu.
U mnogim aplikacijama ovo može biti željeno ponašanje, ali ne i u aplikaciji za poruke! Ne želite da dopustite korisniku da pošalje već otkucanu poruku pogrešnoj osobi zbog slučajnog klika. Da biste ovo popravili, dodajte key
:
<Chat key={to.id} contact={to} />
Ovo osigurava da kada odaberete drugog primaoca, da će Chat
komponenta biti napravljena od nule, uključujući bilo koji state u stablu ispod. React će takođe ponovo kreirati DOM elemente umesto da ih ponovo iskoristi.
Sada promena primaoca uvek briše tekstualno polje:
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' } ];
Deep Dive
U pravoj aplikaciji za poruke, verovatno ćete želeti da povratite input state kada korisnik izabere prethodnog primaoca ponovo. Postoji par načina da držite state “živim” za komponentu koja nije više vidljiva:
- Mogli biste da renderujete sve chat-ove umesto samo jednog, ali da ostale sakrijete pomoću CSS-a. Chat-ovi neće biti uklonjeni iz stabla, pa će njihov lokalni state biti sačuvan. Ovo rešenje radi dobro za jednostavne UI-e. Ali, može postati dosta sporo ako su skrivena stabla velika i sadrže mnogo DOM čvorova.
- Mogli biste podići state i čuvati poruke na čekanju za svakog primaoca u roditeljskoj komponenti. Na ovaj način, nije bitno ako dečje komponente budu uklonjene, jer njihov roditelj čuva bitne informacije. Ovo je najčešće rešenje.
- Takođe možete koristiti i druge izvore informacija, kao ispomoć React state-u. Na primer, verovatno želite da sačuvate draft poruku čak iako korisnik slučajno zatvori stranicu. Da biste to implementirali, mogli biste napraviti da
Chat
komponenta inicijalizuje svoj state čitanjem izlocalStorage
-a, a takođe i da čuva draft poruke tamo.
Nevezano za to koju strategiju odaberete, chat sa Alice je konceptualno drugačiji od chat-a sa Bob-om, tako da ima smisla zadati key
u <Chat>
stablo na osnovu trenutnog primaoca.
Recap
- React čuva state dok god se ista komponenta renderuje na istoj poziciji.
- State se ne čuva u JSX tag-ovima. Povezan je sa pozicijom u stablu na koju stavljate taj JSX.
- Možete forsirati podstablo da resetuje svoj state tako što ćete mu dati drugačiji key.
- Nemojte ugnježdavati definicije komponenti, ili ćete slučajno resetovati state.
Izazov 1 od 5: Popraviti nestajući tekst u input-u
Ovaj primer prikazuje poruku kada kliknete dugme. Međutim, klik na dugme slučajno resetuje input. Zašto se ovo dešava? Popravite to tako da klik na dugme ne resetuje tekst u input-u.
import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return ( <div> <p><i>Pomoć: Vaš omiljeni grad?</i></p> <Form /> <button onClick={() => { setShowHint(false); }}>Sakrij pomoć</button> </div> ); } return ( <div> <Form /> <button onClick={() => { setShowHint(true); }}>Prikaži pomoć</button> </div> ); } function Form() { const [text, setText] = useState(''); return ( <textarea value={text} onChange={e => setText(e.target.value)} /> ); }