Reagovanje na input pomoću stanja
React pruža deklarativan način za manipulisanje UI-jem. Umesto da direktno manipulišete pojedinačnim delovima UI-a, vi opisujete različita stanja u kojima komponenta može biti i menjate ih kao odgovor na korisnički input. Ovo je slično onome kako dizajneri razmišljaju o UI-u.
Naučićete:
- Kako se deklarativno UI programiranje razlikuje od imperativnog UI programiranja
- Kako da nabrojite različita vizuelna stanja u kojima vaša komponenta može biti
- Kako da pokrenete promene između različitih vizuelnih stanja iz koda
Kakav je deklarativan UI u poređenju sa imperativnim
Kada dizajnirate UI interakcije, verovatno razmišljate kako se UI menja kao odgovor na korisničke akcije. Razmotrite formu koja korisniku omogućava da submit-uje odgovor:
- Kad pišete nešto u formu, “Submit” dugme postaje omogućeno.
- Kada pritisnete “Submit”, i forma i dugme postaju onemogućeni, a pojavljuje se spinner.
- Ako mrežni zahtev uspe, forma postaje skrivena, a poruka “Hvala vam” se pojavljuje.
- Ako mrežni zahtev ne uspe, pojavljuje se poruka o grešci, a forma ponovo postaje omogućena.
U imperativnom programiranju gore navedeno direktno odgovara načinu na koji implementirate interakciju. Morate napisati tačne instrukcije za manipulaciju UI-jem na osnovu onoga što se desilo. Evo drugog načina da razmišljate o tome: zamislite da se vozite pored nekoga u autu i govorite mu svaki put gde da skrene.

Illustrated by Rachel Lee Nabors
Ne zna gde želite da idete, već samo prati vaše komande. (I, ako vi promašite smer, završićete na pogrešnom mestu!) Naziva se imperativno jer morate da “komandujete” svaki element, od spinner-a do dugmeta, govoreći računaru kako da ažurira UI.
U ovom primeru imperativnog UI programiranja, forma je napravljena bez React-a. Koristi jedino DOM od pretraživača:
async function handleFormSubmit(e) { e.preventDefault(); disable(textarea); disable(button); show(loadingMessage); hide(errorMessage); try { await submitForm(textarea.value); show(successMessage); hide(form); } catch (err) { show(errorMessage); errorMessage.textContent = err.message; } finally { hide(loadingMessage); enable(textarea); enable(button); } } function handleTextareaChange() { if (textarea.value.length === 0) { disable(button); } else { enable(button); } } function hide(el) { el.style.display = 'none'; } function show(el) { el.style.display = ''; } function enable(el) { el.disabled = false; } function disable(el) { el.disabled = true; } function submitForm(answer) { // Pretvaraj se da koristiš mrežni poziv. return new Promise((resolve, reject) => { setTimeout(() => { if (answer.toLowerCase() === 'istanbul') { resolve(); } else { reject(new Error('Dobar pokušaj, ali pogrešan odgovor. Probaj ponovo!')); } }, 1500); }); } let form = document.getElementById('form'); let textarea = document.getElementById('textarea'); let button = document.getElementById('button'); let loadingMessage = document.getElementById('loading'); let errorMessage = document.getElementById('error'); let successMessage = document.getElementById('success'); form.onsubmit = handleFormSubmit; textarea.oninput = handleTextareaChange;
Manipulisanje UI-jem imperativno radi dovoljno dobro za izolovane slučajeve, ali upravljanje postaje eksponencijalno teže za kompleksnije sisteme. Zamislite da ažurirate stranicu punu formi poput ove. Dodavanje novog UI elementa ili nove interakcije bi zahtevalo pažljivu proveru svog postojećeg koda kako bi bili sigurni da niste napravili bug (na primer, zaboravljanje da se nešto prikaže ili sakrije).
React je napravljen da reši ovaj problem.
U React-u, ne manipulišete UI-jem direktno—što znači da ne omogućavate, onemogućavate, prikazujete niti skrivate komponente direktno. Umesto toga, vi deklarišete šta želite prikazati, a React shvata kako da ažurira UI. Zamislite da uđete u taksi i kažete vozaču gde želite ići umesto da mu govorite gde tačno da skreće. Posao taksiste je da vas odvede tamo, jer možda zna za prečice o kojima niste razmišljali!

Illustrated by Rachel Lee Nabors
Razmišljanje o UI-u deklarativno
Videli ste kako implementirati formu imperativno. Da biste bolje razumeli kako da razmišljate u React-u, ispod ćete ponovo implementirati ovaj UI u React-u:
- Identifikovati različita vizuelna stanja vaše komponente
- Odrediti šta pokreće promene tih stanja
- Predstaviti stanje u memoriji koristeći
useState
- Ukloniti sve neobavezne state promenljive
- Povezati event handler-e za postavljanje state-a
Korak 1: Identifikovati različita vizuelna stanja vaše komponente
U informatici, možda ste čuli da “konačan automat” može biti u jednom od nekoliko “stanja”. Ako radite sa dizajnerom, možda ste videli mockup-e za različita “vizuelna stanja”. React se nalazi na preseku dizajna i informatike, tako da su obe ideje izvor inspiracije.
Prvo, morate vizualizovati sva različita “stanja” koje korisnik može videti na UI-u:
- Prazno: Forma ima onemogućeno “Submit” dugme.
- Pisanje: Forma ima omogućeno “Submit” dugme.
- Submit-ovanje: Forma je potpuno onemogućena. Prikazan je spinner.
- Uspešno: Poruka “Hvala vam” je prikazana umesto forme.
- Greška: Kao i stanje “Pisanje”, ali sa dodatnom porukom o grešci.
Baš kao i dizajner, želećete da “mock-ujete” ili pravite “mock-ove” za različita vizuelna stanja pre nego što dodate logiku. Na primer, ovde je mock samo za vizuelni deo forme. Ovaj mock se kontroliše pomoću prop-a pod imenom status
čija default vrednost je 'empty'
:
export default function Form({ status = 'empty' }) { if (status === 'success') { return <h1>To je tačno!</h1> } return ( <> <h2>Kviz gradova</h2> <p> U kom gradu je bilbord koji pretvara vazduh u pijaću vodu? </p> <form> <textarea /> <br /> <button> Submit </button> </form> </> ) }
Možete nazvati taj prop kako god želite, imenovanje nije bitno. Probajte da promenite sa status = 'empty'
na status = 'success'
da biste videli uspešnu poruku. Mock-ovanje vam omogućava da brzo iterirate kroz UI pre nego što dodate bilo kakvu logiku. Ovde je detaljniji primer iste komponente, i dalje “kontrolisan” preko status
prop-a:
export default function Form({ // Probajte 'submitting', 'error', 'success': status = 'empty' }) { if (status === 'success') { return <h1>To je tačno!</h1> } return ( <> <h2>Kviz gradova</h2> <p> U kom gradu je bilbord koji pretvara vazduh u pijaću vodu? </p> <form> <textarea disabled={ status === 'submitting' } /> <br /> <button disabled={ status === 'empty' || status === 'submitting' }> Submit </button> {status === 'error' && <p className="Error"> Dobar pokušaj, ali pogrešan odgovor. Probaj ponovo! </p> } </form> </> ); }
Deep Dive
Ako komponenta ima mnogo vizuelnih stanja, može biti zgodno prikazati ih sve na jednoj stranici:
import Form from './Form.js'; let statuses = [ 'empty', 'typing', 'submitting', 'success', 'error', ]; export default function App() { return ( <> {statuses.map(status => ( <section key={status}> <h4>Forma ({status}):</h4> <Form status={status} /> </section> ))} </> ); }
Stranice poput ovih se često nazivaju “living styleguides” ili “storybooks”.
Korak 2: Odrediti šta pokreće promene tih stanja
Možete pokrenuti ažuriranje stanja kao odgovor na dve vrste input-a:
- Ljudski input-i poput kliktanja dugmeta, pisanja u tekstualno polje, navigiranja na link.
- Računarski input-i poput dobijanja mrežnog odgovora, završavanja timeout-a, učitavanja slike.


Illustrated by Rachel Lee Nabors
U oba slučaja, morate postaviti state promenljive da biste ažurirali UI. Za formu koju razvijate, trebaćete da promenite stanje kao odgovor na više različitih input-a:
- Promena tekstualnog input-a (ljudski) treba da menja stanje Prazno u stanje Pisanje i obrnuto, u zavisnosti od toga da li je tekstualno polje prazno ili ne.
- Klik na Submit dugme (ljudski) treba da promeni stanje u Submit-ovanje.
- Uspešan mrežni odgovor (računarski) treba da promeni stanje u Uspešno.
- Neuspešan mrežni odgovor (računarski) treba da promeni stanje u Greška sa odgovarajućom porukom o grešci.
Da biste lakše vizualizovali ovaj tok, probajte da svako stanje crtate kao označeni krug, a svaku promenu stanja kao strelicu. Na ovaj način možete skicirati mnoge tokove stanja i odvojiti bug-ove mnogo pre implementacije.


Stanja forme
Korak 3: Predstaviti stanje u memoriji koristeći useState
Sada je potrebno da predstavite vizuelna stanja u memoriji koristeći useState
. Ključ je jednostavnost: svaki deo state-a je “pokretni deo”, a želite što manje potrebnih “pokretnih delova”. Veća kompleksnost uvodi više bug-ova!
Počnite sa state-om koji apsolutno mora biti tu. Na primer, moraćete da čuvate answer
za input kao i error
(ako postoji) da čuvate poslednju grešku:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
Onda, biće vam potrebna state promenljiva da predstavite koje vizuelno stanje želite da prikažete. Obično postoji više od jednog načina da to predstavite u memoriji, tako da morate eksperimentisati.
Ako se mučite da odmah smislite najbolji način, počnite sa dodavanjem onoliko state-ova da definitivno osigurate da su sva vizuelna stanja pokrivena:
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
Vaša prva ideja vrlo verovatno neće biti najbolja, ali to je u redu—refaktorisanje state-a je deo procesa!
Korak 4: Ukloniti sve neobavezne state promenljive
Želite da izbegnete dupliranje unutar state-a tako da pratite samo ono što je obavezno. Ako potrošite malo vremena na refaktorisanje strukture vašeg state-a učinićete vaše komponente lakšim za razumevanje, smanjićete dupliranje i izbeći nenamerna značenja. Vaš cilj je da sprečite slučaj u kojem state u memoriji ne predstavlja nijedan validan UI koji želite da korisnik vidi. (Na primer, ne želite da prikažete poruku o grešci i onemogućite input istovremeno, pošto korisnik neće moći da ispravi grešku!)
Evo nekih pitanja koja možete postaviti za svoje state promenljive:
- Da li ovaj state stvara paradoks? Na primer,
isTyping
iisSubmitting
ne mogu zajedno bititrue
. Paradoks uglavnom znači da state nije dovoljno ograničen. Postoje četiri moguće kombinacije dve boolean vrednosti, ali samo tri odgovaraju validnim stanjima. Da biste uklonili “nemoguće” stanje, možete ih ukombinovati ustatus
koji mora imati jednu od tri vrednosti:'typing'
,'submitting'
ili'success'
. - Da li je ista informacija već dostupna u drugoj state promenljivoj? Još jedan paradoks:
isEmpty
iisTyping
ne mogu bititrue
istovremeno. Ako ih napravite kao odvojene state promenljive rizikujete da ne budu sinhronizovane i pravite bug-ove. Srećom, možete uklonitiisEmpty
i umesto toga proveritianswer.length === 0
. - Da li istu informaciju možete dobiti inverzijom druge state promenljive?
isError
nije potreban jer možete proveritierror !== null
.
Nakon ovog čišćenja, ostajete sa 3 (od 7!) obaveznih state promenljivih:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting' ili 'success'
Znate da su obavezne, jer nijednu ne možete ukloniti a da ne skršite funkcionalnost.
Deep Dive
Ove tri promenljive su dovoljno dobre da predstavljaju stanje naše forme. Međutim, i dalje postoje neka posredna stanja koji nemaju skroz smisla. Na primer, error
koji nije null nema smisla dok je status
jednak 'success'
. Da biste modelovali state preciznije, možete ga izdvojiti u reducer. Reducer-i vam omogućavaju da objedinite state promenljive u jedan objekat i grupišete svu povezanu logiku!
Korak 5: Povezati event handler-e za postavljanje state-a
Na kraju, kreirajte event handler-e koji ažuriraju state. Ispod je finalna forma sa svim povezanim event handler-ima:
import { useState } from 'react'; export default function Form() { const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); if (status === 'success') { return <h1>To je tačno!</h1> } async function handleSubmit(e) { e.preventDefault(); setStatus('submitting'); try { await submitForm(answer); setStatus('success'); } catch (err) { setStatus('typing'); setError(err); } } function handleTextareaChange(e) { setAnswer(e.target.value); } return ( <> <h2>Kviz gradova</h2> <p> U kom gradu je bilbord koji pretvara vazduh u pijaću vodu? </p> <form onSubmit={handleSubmit}> <textarea value={answer} onChange={handleTextareaChange} disabled={status === 'submitting'} /> <br /> <button disabled={ answer.length === 0 || status === 'submitting' }> Submit </button> {error !== null && <p className="Error"> {error.message} </p> } </form> </> ); } function submitForm(answer) { // Pretvaraj se da koristiš mrežni poziv. return new Promise((resolve, reject) => { setTimeout(() => { let shouldError = answer.toLowerCase() !== 'lima' if (shouldError) { reject(new Error('Dobar pokušaj, ali pogrešan odgovor. Probaj ponovo!')); } else { resolve(); } }, 1500); }); }
Iako je ovaj kod duži od originalnog imperativnog primera, manje je krt. Izražavanje svih interakcija kao promena stanja vam omogućava da kasnije uvedete nova vizuelna stanja bez da pokvarite postojeća. Takođe vam omogućava da promenite šta je prikazano u svakom stanju bez promene logike u samoj interakciji.
Recap
- Deklarativno programiranje označava opisivanje UI-a za svako vizuelno stanje, a ne mikromenadžment nad UI-jem (imperativno).
- Kada razvijate komponentu:
- Identifikujte sva njena vizuelna stanja.
- Odredite ljudske i računarske pokretače promena stanja.
- Modelujte stanje sa
useState
. - Uklonite neobavezne state-ove da izbegnete bug-ove i paradokse.
- Povežite event handler-e da postavite state.
Izazov 1 od 3: Dodati i ukloniti CSS klasu
Napravite da klik na sliku uklanja background--active
CSS klasu iz spoljašnjeg <div>
-a, ali dodaje picture--active
klasu u <img>
. Klik na pozadinu bi trebao da povrati originalne CSS klase.
Vizuelno, trebate očekivati da klik na sliku uklanja ljubičastu pozadinu i ističe ivicu slike. Klik van slike ističe pozadinu, ali uklanja istaknutu ivicu slike.
export default function Picture() { return ( <div className="background background--active"> <img className="picture" alt="Dugine kuće u Kampung Pelangi, Indonezija" src="https://i.imgur.com/5qwVYb1.jpeg" /> </div> ); }