Komponente sa stanjem

Komponente sa stanjem

Za pravilno razumijevanje pojma stanja komponente i načina na koji React osvježava dijelove sučelja aplikacije, moramo se upoznati sa pojmom virtualnog DOM-a.

Virtualni DOM

React uvodi pojam virtualnog DOM-a kojeg možete zamisliti kao kopiju originalnog DOM-a (Document Object Model (opens in a new tab)). Svaki put kada napravimo promjenu u našoj React aplikaciji, zapravo mijenjamo samo stanje virtualnog DOM-a, a ne stvarnog. Nakon što React detektira promjenu na virtualnom DOM-u usporediti će ga sa stvarnim kako bi dobio razliku (diff) između dva DOM-a. React zatim računa optimalni način na koji može te promjene uvesti u stvarni DOM. Iako na prvu ovaj proces izgleda složeniji u stvarnosti je brži od direktne (neoptimizirane) manipulacije stvarnog DOM-a. Postoje i alternativni web okviri koji odbacuju koncept virtualnog DOM-a ali oni također imaju posebno razvijene načine na koji se osvježava prikaz.

React i virtualni DOM

Svaki dio korisničkog sučelja u Reactu je predstavljen u obliku komponente i svaka komponenta ima svoje unutarnje stanje. React prati svoje komponente i osluškuje promjene stanja. Kada se stanje neke komponente promijeni, React osvježava virtualno DOM stablo. Nakon što je stablo osvježeno, React uspoređuje trenutno stanje virtualnog DOM-a sa prethodnim stanjem virtualnog DOM-a. Ovaj proces uspoređivanja se naziva diffing.

Nakon što je React saznao koji dijelovi virtualnog DOM-a su se promijenili, React će osvježiti samo te objekte u stvarnom DOM-u. Dobra stvar je što je to sve događa u pozadini, bez potrebe za intervencijom programera. Zadatak programera je samo osvježiti stanje (vrijednosti) komponente, a React biblioteka će se pobrinuti za tehnički dio na koji način će se to izvesti.

Proces osvježavanja

Grupno (batch) osvježavanje

Još jedna od prednosti Reacta je što koristi tzv batch update mehanizam koji također doprinosi boljim performansama aplikacije. Radi se o pristupu u kojem se osvježavanje stanja ne radi uvijek trenutno, već u serijama. Ukoliko u jednom trenutku promijenimo dvije vrijednosti u našoj aplikaciji - ona se neće osvježiti dva puta (po jednom za svaku promjenu) već će obje promjene biti poslane zajedno i aplikacija će se osvježiti samo jednom. Primjer ovakvog ponašanja ćemo vidjeti na idućim primjerima.

React za upravljanje stanjima funkcijskih komponenti koristi metodu useState koja spada u kategoriju tzv. React hooks. React hooks su posebne funkcije koje su vezane uz upravljanje stanjima i životnim ciklusom (lifecycle) React komponenti i o njima ćemo detaljnije pričati u idućoj lekciji dok ćemo se sada fokusirati isključivo na useState.

Stanje komponente

Vratimo se na malo jednostavniji oblik komponente u kojoj samo prikazujemo vrijednost varijable koja je deklarirana i inicijalizirana unutar same komponente.

App.jsx
import "./App.css";
function App() {
  var broj = 5;
  return (
    <div>
      <p>Vrijednost broja: {broj}</p>
    </div>
  );
}
export default App;

Iako je varijabla broj deklarirana i inicijalizirana unutar same komponente, ona ne predstavlja njeno unutarnje stanje. Što ako želimo uvećati vrijednost brojača? Mogli bi dodati poziv funkcije koji će promijeniti vrijednost varijable:

App.jsx
import "./App.css";
function App() {
  var broj = 5;
 
  function uvecajBrojac() {
    broj = 7;
    console.log('Broj je ', broj);
  }
  return (
    <div>
      <p>Vrijednost broja: {broj}</p>
    </div>
  );
}

Postoje dva problema sa ovim pristupom:

  1. promjena vrijednosti varijable broj neće uzrokovati novo renderiranje komponente (što možemo i sami vidjeti).
  2. čak kada bi i nekako uspjeli izazvati ponovno renderiranje, problem je što su React komponente same po sebi immutable tj. nepromjenjive. Ponovno renderiranje komponente znači da će se kôd koji pripada njenom funkcijskom obliku ponovno izvršiti, što bi u ovom slučaju značilo da će se varijabla broj ponovno inicijalizirati na početnu vrijednost (5).

Ono što mi želimo postići je upravo suprotno - želimo komponenti omogućiti da pamti vrijednost koja će ostati sačuvana između uzastopnih renderiranja te čija će promjena automatski pokrenuti proces renderiranja kako bi se mogla prikazati nova vrijednost. To u Reactu možemo postići korištenjem metode useEffect koja nam pruža dva elementa:

  • varijablu stanja (state variable) unutar koje možemo spremiti vrijednost koju želimo očuvati
  • funkciju za postavljanje stanja (state setter) pomoću koje možemo mijenjati vrijednost varijable stanja, ali na kontrolirani način tako da React može zakazati promjene na DOM-u

useState

Modificirati ćemo prethodni primjer tako da našoj komponenti dodamo jednu varijablu stanja (broj):

App.jsx
import { useState } from "react";
import "./App.css";
 
function App() {
  const [broj, postaviBroj] = useState(5);
 
  return (
    <div>
      <p>Vrijednost broja: {broj}</p>
    </div>
  );
}
export default App;

Pogledajmo detaljnije što smo napravili:

  1. Prvi korak je iz React biblioteke uključiti hook funkciju koju želimo koristiti. To smo ovdje napravili odmah na početku i uključili useState
  2. Zatim smo unutar tijela komponente pozvali useState i kao argument joj poslali inicijalnu vrijednost varijable stanja (u ovom slučaju broj 5)
  3. Kao što smo već spomenuli, useState funkcija kao rezultat vraća niz sa dva elementa - prvi element je sama varijabla stanja koja sadrži inicijalnu vrijednost, a drugi element je referenca na funkciju pomoću koje možemo mijenjati tu inicijalnu vrijednost tj. našu varijablu stanja. Naravno da su nam potrebni oboje, pa spremamo i varijablu i funkciju u lokalne konstante unutar komponente koje imenujemo po želji (u ovom slučaju nam se varijabla stanja zove broj a funkcija za promjenu postaviBroj)

U našem primjeru već koristimo varijablu stanja u prikazu komponente. Nedostaje nam još logika promjene te vrijednosti. Dodati ćemo unutar komponente novi "button" element i napisati praznu pomoćnu funkciju koju ćemo vezati uz onClick atribut (sjetite se pravila za pisanje atributa u JSX-u):

App.jsx
function App() {
  const [broj, postaviBroj] = useState(5);
  function uvecajBrojac() {
    // Uvecaj brojac
  }
  return (
    <div>
      <p>Vrijednost broja: {broj}</p>
      <button onClick={uvecajBrojac}>Uvećaj</button>
    </div>
  );
}

Promjena stanja

Naravno, nedostaje nam još samo logika uvećavanja brojača - za početak ćemo krenuti sa najjednostavnijom verzijom - dovoljno je pozvati funkciju za promjenu stanja varijable i kao argument joj poslati novu vrijednost koju želimo spremiti u varijablu stanja:

App.jsx
function uvecajBrojac() {
  postaviBroj(6);
}

Očito je da na ovaj način broj možemo uvećati samo jednom, bolja bi opcija bila da brojač možemo uvećavati više puta. Funkciju smo mogli napisati i ovako:

App.jsx
function uvecajBrojac() {
  postaviBroj(broj + 1);
}

Vodite računa da smo kao argument poslali izračun u kojem zbrajamo trenutnu vrijednost varijable stanja i konstantu 1. Pri tome nismo direktno mijenjali samu vrijednost varijable stanja - nismo pisali "broj += 1" ili "broj++".

Ovaj način slanja novog stanja u obliku konstante ili izračuna nije uvijek najbolja opcija ali na to ćemo se osvrnuti kasnije.

⚠️

U nijednom slučaju ne bi trebali direktno mijenjati vrijednost varijable stanja, već uvijek za to koristiti pripadajuću funkciju. Zbog toga je dobra praksa varijablu stanja spremati u const deklaraciju kao u ovom primjeru jer ćete tada dobiti upozorenje da pokušavate mijenjati konstantu, što nije moguće.

Osvježavanje prikaza

Kao što možete primijetiti na aplikaciji, svaki put kada pozovemo funkciju za promjenu stanja, aplikacija se automatski osvježi i prikaže novu vrijednost varijable stanja na zaslonu. Već smo prethodno spomenuli da React to samostalno odrađuje u pozadini, naš zadatak kao programera je samo postaviti novo stanje korištenjem pripadajuće funkcije.

Osvrnimo se na još jedan zanimljiv primjer. Unutar komponente ćemo iskoristiti JS funkciju setTimeout kako bi zakazali promjenu stanja nakon određenog broja sekundi. Sada naša "App" komponenta izgleda ovako:

App.jsx
function App() {
  const [broj, postaviBroj] = useState(5);
 
  function uvecajBrojac() {
    postaviBroj(broj + 1);
  }
  setTimeout(uvecajBrojac, 1500);
 
  return (
    <div>
      <p>Vrijednost broja: {broj}</p>
      <button onClick={uvecajBrojac}>Uvećaj</button>
    </div>
  );
}

Možete li zaključiti zašto je rezultat brojač koji se stalno uvećava, iako smo koristili setTimeout, a ne setInterval od koje bi se očekivao takav efekt.

Odgovor leži u načinu na koji React osvježava komponente. Krenimo od početka u koracima:

  • na početku imamo prazni prozor preglednika. React zatim izvršava prvo iscrtavanje (renderiranje) naše App komponente. Prilikom tog prvog renderiranja izvršava se sav kôd unutar komponente, uključujući i poziv setTimeout funkcije koja zakazuje pokretanje funkcije za uvećanje brojača nakon 1500 ms (1.5 sekundi).
  • nakon što istekne setTimeout vrijeme, poziva se pomoćna funkcija uvecajBrojac koja mijenja vrijednost varijable stanja
  • React detektira promjenu unutarnjeg stanja komponente i zaključuje da je istu potrebno osvježiti tj. ponovno je renderirati kako bi se na sučelju prikazalo aktualno stanje komponente - pokreće se proces ponovnog renderiranja
  • ponovno renderiranje uzrokuje

Uvjetno renderiranje

Nakon što smo se upoznali sa konceptom stanja možemo spomenuti i jedan "trik" koji će često koristi pri izradi React aplikacija. Radi se o pojmu uvjetnog renderiranja koji podrazumijeva da ovisno o nekoj vrijednosti (stanju) mijenjamo izgled komponente ili čak prikazujemo različite komponente.

Krenimo sa jednostavnim primjerom i već poznatim brojačem. Želimo korisnicima ispisati upute da pritiskom na tipku mogu uvećati vrijednost brojača. Jednom kada su korisnici uvećali brojač, više nema potrebe da se poruka ispisuje. Jedan od načina kako postići tu funkcionalnost je ovakav:

App.jsx
import { useState } from "react";
import "./App.css";
 
function App() {
  const [broj, postaviBroj] = useState(0);
 
  return (
    <div className='main'>
      <p>Broj: {broj}</p>
      <button onClick={() => postaviBroj(broj + 1)}>+</button>
      <button onClick={() => postaviBroj(0)}>Reset</button>
      {broj == 0 && <small>Pritisnite tipku "+" za uvećanje brojača!</small>}
    </div>
  );
}
 
export default App;

U ovom slučaju koristimo logički operator && kako bi postigli učinak uvjetnog renderiranja. Unutar {} zagrada pišemo logički izraz u kojem se sa lijeve strane nalazi uvjet kojeg provjeravamo, a sa desne JSX kôd kojeg želimo uvjetno renderirati. Ako je lijeva strana izraza istinita (true) onda će se desni dio prikazati u sklopu komponente (on je uvijek true). Ako je lijevi dio izraza false onda se cijeli izraz evaluira u false (jer koristimo operator AND) i React će to zanemariti prilikom renderiranja.

Broj: 0

Pritisnite tipku "+" za uvećanje brojača!

Za drugi primjer ćemo napraviti dodatnu komponentu "Zadatak i ovisno o props vrijednosti vraćati različiti izgled komponente:

Zadatak.jsx
function Zadatak(props) {
  if (props.gotov) {
    return <p>{props.natpis} ✅</p>;
  }
  return <p>{props.natpis} 🕓🔄</p>;
}
export default Zadatak;
App.jsx
import Zadatak from "./Zadatak";
 
function App() {
  return (
    <div>
      <h2>Popis zadataka:</h2>
      <Zadatak natpis="Zaliti cvijeće" gotov={true} />
      <Zadatak natpis="Kupiti kruh" gotov={false} />
      <Zadatak natpis="Upisati React tečaj" gotov={true} />
    </div>
  );
}
export default App;

Popis zadataka:

Zaliti cvijeće

Kupiti kruh 🕓🔄

Upisati React tečaj

U ovom konkretnom primjeru nismo koristili varijablu stanja nego props vrijednost za uvjetno renderiranje ali na isti način je uvjet mogao biti vezan uz neko stanje komponente.

Svi prethodni primjeri su sadržavali komponente sa samo jednom varijablom stanja. U praksi ćemo rijetko kada imati tako "čistu" situaciju, uobičajeno je da komponente imaju više state varijabli pa ćemo se u idućoj lekciji detaljnije osvrnuti i na takve primjere. Za početak nam je dovoljno pogledati primjer u kojem naša komponenta sadrži dva interna stanja.

Ponovno koristimo primjer sa brojačem, samo ovaj put imamo dva različita brojača tj. dvije različite varijable stanja.

App.jsx
function App() {
  const [broj, postaviBroj] = useState(5);
  const [drugi, postaviDrugi] = useState(99)
 
  function uvecajPrviBrojac() {
    postaviBroj(broj + 1);
  }
 
  return (
    <div>
      <p>Vrijednost prvog broja: {broj}</p>
      <button onClick={uvecajPrviBrojac}>Uvećaj prvi </button>
      <p>Vrijednost drugog broja: {drugi}</p>
      <button onClick={() => postaviDrugi(drugi + 1)}>Uvećaj drugi </button>
    </div>
  );
}

Možete uočiti da smo umjesto pisanja pomoćne funkcije jednostavno iskoristili ES6 mogućnost pisanja arrow funkcija i poslali anonimnu funkciju kao argument onClick svojstvu. U ovom slučaju imamo dva brojača i osvježavanjem bilo kojeg od njih osvježiti će cijela komponenta ali će drugi brojač zadržati trenutnu vrijednost (neće se vratiti na početnu vrijednost) - to je jedna od ključnih karakteristika useState-a i korištenja internog stanja.

Dok je god komponenta aktivna i prikazana na ekranu, vrijednost varijable stanja će ostati sačuvana između višestrukih ciklusa renderiranja (osim naravno ako renderiranje nije uzrokovano promjenom baš te varijable stanja.)

Digitalna Dalmacija