Stanja komponente
Na prethodnoj radionici smo se već upoznali sa useState hook-om. Ovaj hook nam služi kako bi komponentama dodali mogućnost pamćenja neke vrijednosti (varijabla stanja). Promjena varijable stanja uzrokuje ponovno renderiranje komponente kako bi se prikazala nova vrijednost.
Jedna od značajki osvježavanja stanja u Reactu je da se ono zakazuje i odvija u grupama (batches). Vidjeli smo primjer u kojem smo uvećavali dva brojača unutar iste funkcije i kako će React istovremeno osvježiti i prikazati oba stanja. Sada ćemo još malo detaljnije pogledati način na koji React osvježava stanja i kako pisati funkcije za osvježavanje.
Vratimo se na jednostavni primjer sa brojačem. Unutar komponente definiramo stanje pomoću useState. Rezultat destrukturiramo u varijablu stanja brojac i funkciju za promjenu stanja postaviBrojac. Napisati ćemo i pomoćnu funkciju koju ćemo povezati sa onClick atributom:
import { useState } from 'react'
import './App.css'
function App() {
const [brojac, postaviBrojac] = useState(0)
function uvecajBrojac(){
postaviBrojac(brojac + 1)
}
return (
<div>
<p>Broj: {brojac}</p>
<button onClick={uvecajBrojac}>+</button>
</div>
)
}
export default AppMožemo to probati i na interaktivnom primjeru:
Broj: 0
Uzastopno osvježavanje
Ovo nam je sve poznato i od prije. Sada ćemo napraviti malu promjenu i u našoj pomoćnoj funkciji dva puta pozvati funkciju za uvećanje brojača. Ponovno imamo ispod interaktivni primjer, a prije nego ga testiramo pokušajte predvidjeti koji će biti rezultat nakon prvog pritiska na "+".
import { useState } from 'react'
import './App.css'
function App() {
const [brojac, postaviBrojac] = useState(0)
function uvecajBrojac(){
postaviBrojac(brojac + 1)
postaviBrojac(brojac + 1)
postaviBrojac(brojac + 1)
}
return (
<div>
<p>Broj: {brojac}</p>
<button onClick={uvecajBrojac}>+</button>
</div>
)
}
export default AppBroj: 0
Problem je u već spomenutom načinu na koji React renderira komponente i osvježava stanje. Svako stanje komponente možemo zamisliti kao jednu snimku (snapshot) koja je prikazana (renderirana) na ekranu. Ta snimka je statična, sa fiksnim vrijednostima, sve dok ne zatražimo od Reacta da promjeni vrijednost neke varijable stanja i na zaslonu prikaže novu snimku sa osvježenim vrijednostima.
Pozivom funkcije postaviBrojac(brojac + 1) računamo zbroj trenutne vrijednosti brojača (0) i konstante 1 i zapravo pozivamo funkciju postaviBrojac(1) . U tom trenutku smo samo zakazali promjenu varijable stanja, nismo je promijenili. Drugi uzastopni poziv funkcije postaviBrojac(brojac + 1) i dalje za izračun koristi vrijednost brojača iz trenutnog snapshot-a, a ne vrijednost koju će stanje poprimiti nakon idućeg renderiranja.
U gornjem primjeru smo zapravo tri puta pozvali funkciju postaviBrojac(1) i zbog toga se nakon prvog klika brojač postavi na 1, a ne na 3. Isto tako će svaki idući pritisak na "+" uvećati brojač samo za 1, a ne za 3.

Funkcija osvježavanja
Iako je gornji primjer neobičan (mogli smo jednostavno napisati brojac + 3) korisno je naučiti kako riješiti ovaj problem jer se možete naći u situaciji kada morate više puta promijeniti jedno stanje prije idućeg renderiranja (takve situacije su rijetke ali se događaju).
Rješenje je da vrijednost novog stanja ne šaljemo kao konstantu već kao tzv. updater funkciju koja iduće stanje računa na temelju trenutnog stanja. Ta se funkcija dodaje u red izvršavanja (queue) i evaluirati će se tek u trenutku kada React započne sa izračunom novog stanja, a ne u trenutku poziva kao u gornjem primjeru. Samim time updater funkcija kao argument prima trenutnu vrijednost varijable stanja u memoriji, a ne u snapshotu.
Pogledajmo primjer:
function App() {
const [brojac, postaviBrojac] = useState(0)
function uvecajBrojac(){
postaviBrojac(trenutnoStanje => trenutnoStanje + 1)
postaviBrojac((trenutnoStanje) => { return trenutnoStanje + 1})
postaviBrojac(br => br + 1)
}
return (
<div>
<p>Broj: {brojac}</p>
<button onClick={uvecajBrojac}>+</button>
</div>
)
}Sada funkciji postaviBrojac kao argument ne šaljemo izraz koji se odmah evaluira (npr brojac + 1) već šaljemo funkciju. Funkcija kao parametar prima trenutnu vrijednost stanja kojeg osvježavamo, a kao povratnu vrijednost mora primiti novu vrijednost stanja. Srednji poziv prikazuje funkciju u punom obliku, a prvi i treći u skraćenom obliku kojeg nam omogućuje JS sintaksa kada imamo samo jedan argument i povratni izraz. Brojač nam sada radi ispravno:
Broj: 0
Sinkronizacija osvježavanja
Još jedno bitno svojstvo Reacta je da pozivom funkcije za promjenu varijable stanja ne dolazi nužno do trenutne promjene stanja (i ponovnog renderiranja). Umjesto toga, možemo zamisliti da jednostavno zakazujemo promjenu za neki idući diskretni trenutak u kojem React odluči da je potrebno osvježiti stanje aplikacije.
Prednost ovog pristupa je što se na taj način osvježavanje stanja može optimizirati. To posebno dolazi do izražaja kada u kratkom razdoblju mijenjamo više varijabli stanja. Umjesto višestrukih pojedinačnih izmjena, koje bi svaka za sebe uzrokovala novo renderiranje, React jednostavno odrađuje sve promjene zajedno i na taj način je potreban samo jedan ciklus ponovnog renderiranja.
Ovo je najbolje samostalno probati na primjeru. Umjesto da prethodno definirana dva brojača povećavamo pojedinačno, napisati ćemo funkciju uvecajOba unutar koje ćemo pozvati obje funkcije za promjenu varijabli stanja. Kako bi bolje demonstrirali sinkronizirano osvježavanje, između dva poziva ćemo pozvati još jednu funkciju čija je svrha samo usporiti aplikaciju sa velikim brojem iteracija petlje. Pokušaje pokrenuti ovaj primjer:
function trosiVrijeme(){
for (let i = 0; i < 1000000000; i ++){
if (i % 10000000 == 0){
console.log("Poruka")
}
}
}
function App() {
const [broj, postaviBroj] = useState(5);
const [drugi, postaviDrugi] = useState(99)
function uvecajOba(){
postaviBroj(broj + 1)
trosiVrijeme()
postaviDrugi(drugi + 1)
}
return (
<div>
<p>Vrijednost prvog broja: {broj}</p>
<p>Vrijednost drugog broja: {drugi}</p>
<button onClick={uvecajOba}>Uvećaj oba </button>
</div>
);
}Funkcija "uvecajOba" sadrži unutar sebe tri poziva:
- uvećanje prvog brojača
- poziv funkcije sa "velikom" petljom
- uvećanje drugog brojača
Praćenjem ispisa u konzoli možemo vidjeti da se u istoj počinju prikazivati poruke, a da su na ekranu prikazane početne vrijednosti oba brojača. U trenutku kada se poruke počnu ispisivati, jasno je da smo došli do poziva funkcije "trosiVrijeme" i da je funkcija za povećanje prvog broja već pozvana. Unatoč tome, vrijednost brojača se ne uvećava sve dok se petlja ne izvrši do kraja i dođemo do poziva druge funkcije za promjenu stanja. Tek tada React izvršava obje promjene stanja i ponovno renderira komponentu - u tom trenutku vidimo da su oba brojača uvećana.
Bez ovog svojstva, prvi brojač bi se odmah uvećao što bi uzrokovalo ponovno renderiranje komponente i tada bi se našli u problemu - za vrijeme renderiranja bi se i dalje trebala izvršavati funkcija sa petljom te bi nam ostao još poziv druge funkcije za promjenu stanja koju treba pozvati. Zapravo bi imali na ekranu prikazano neko među-stanje aplikacije dok bi se još uvijek izvršavale naredbe iz prethodnog stanja. Tu dolazimo do pitanja što se događa sa korisničkim sučeljem dok još nisu sve promjene izvršene. Blokiranje sučelja nije dobra opcija jer bi korisnik stekao dojam neispravnog rada aplikacije. Interaktivno sučelje također nije dobra ideja jer korisnik ponovno može pritisnuti tipku za uvećanje oba brojača - prije nego se drugi brojač stigao uvećati. To bi izazvalo dodatne probleme - kojim redoslijedom sada izvršiti promjene stanja i renderiranje? Nadalje, komponente često šalju svoje vrijednosti drugim komponentama i bez sinkroniziranih promjena možemo doći do situacije da nemamo konzistentne vrijednosti unutar naše aplikacije, što bi naravno gotovo sigurno dovelo do neispravnog rada same aplikacije.
Stanje kao objekt
Varijabla stanja ne mora nužno sadržavati samo jednu vrijednost. Kao i u "klasičnu" varijablu, u varijablu stanja možemo spremati različite tipove podataka. Prethodni primjer sa dva brojača možemo napisati na način da imamo samo jednu varijablu stanja koja sadrži objekt sa dva svojstva:
function App() {
const [brojaci, postaviBrojace] = useState({prvi: 5, drugi: 10});
return (
<div>
<p>Vrijednost prvog broja: {brojaci.prvi}</p>
<p>Vrijednost drugog broja: {brojaci.drugi}</p>
</div>
);
}Kod korištenja objekata za spremanje stanja potrebno je voditi računa o načinu na koji radimo promjene stanja. Za početak ćemo samo dodati nove button elemente i povezati ih sa praznim pomoćnim funkcijama:
function App() {
const [brojaci, postaviBrojace] = useState({prvi: 5, drugi: 10});
function uvecajPrvi(){
//
}
function uvecajDrugi(){
//
}
return (
<div>
<p>Vrijednost prvog broja: {brojaci.prvi}</p>
<button onClick={uvecajPrvi}>Uvećaj prvi</button>
<p>Vrijednost drugog broja: {brojaci.drugi}</p>
<button onClick={uvecajDrugi}>Uvećaj drugi</button>
</div>
);
}U ovom trenutku se potrebno podsjetiti da su stanja komponenti immutable tj. nepromjenjiva. U praksi to znači da ne možemo direktno mijenjati stanje objekta. Ovo ne bi bilo ispravno:
Ne smijemo direktno mijenjati varijablu stanja
function uvecajPrvi(){
postavibrojace(brojaci.prvi = brojaci.prvi + 1)
}Druga potencijalna pogreška može nastati ako šaljemo novi objekt kao iduće stanje, ali ne uključimo sva svojstva originalnog objekta stanja.
Prvi brojač će se ispravno uvećati ali ćemo izgubiti vrijednost drugog brojača.
function uvecajPrvi(){
postaviBrojace({prvi: brojaci.prvi + 1})
}Ispravni način je naravno poslati novi objekt koji sadrži sva svojstva kao i originalno stanje ali sa novim vrijednostima:
function uvecajPrvi(){
const novi = {
prvi: brojaci.prvi + 1,
drugi: brojaci.drugi
}
postaviBrojace(novi)
}Kao što možete vidjeti, prvo smo napravili novi objekt sa istim svojstvima. Za vrijednost prvog svojstva smo uzeli vrijednost iz trenutnog stanja i uvećali je, dok smo za drugo svojstvo ostavili originalnu vrijednost (jer uvećavamo samo prvi brojač). Analogno ovome, možemo napisati funkciju i za uvećanje drugog brojača, samo ćemo ovaj put iskoristiti pogodnosti ES6 sintakse i pomoću spread operatora napraviti kopiju originalnog objekta - koju onda možemo mijenjati.
function uvecajDrugi(){
let novi = {...brojaci}
novi.drugi += 1
postaviBrojace(novi)
}Na ovaj način smo izbjegli potrebu za prepisivanjem svih svojstava koje ne želimo mijenjati (i njihovih vrijednosti). Jednostavno smo napravili kopiju postojećeg stanja, primijenili sve promjene na toj kopiji te je zatim iskoristili kako bi Reactu naznačili da je to novo stanje komponente koje želimo imati.
