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.

U autu koji vozi osoba uznemirenog izgleda koja predstavlja JavaScript, putnik naređuje vozaču da izvrši niz komplikovanih skretanja.

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!

U autu koji vozi React, putnik traži da bude odvezen na specifično mesto na mapi. React shvata kako da to uradi.

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:

  1. Identifikovati različita vizuelna stanja vaše komponente
  2. Odrediti šta pokreće promene tih stanja
  3. Predstaviti stanje u memoriji koristeći useState
  4. Ukloniti sve neobavezne state promenljive
  5. 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

Prikazivanje više vizuelnih stanja istovremeno

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.
Prst.
Ljudski input-i
Jedinice i nule.
Računarski input-i

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.

Napomena

Primetite da ljudski input-i često zahtevaju event handler-e!

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.

Dijagram toka od leva ka desno sa 5 čvorova. Prvi čvor, označen sa 'empty', ima jedan prelaz, označen sa 'start typing', koji se povezuje sa čvorom označenim sa 'typing'. Taj čvor ima jedan prelaz, označen kao 'press submit', koji se povezuje sa čvorom koji je označen kao 'submitting', koji ima dva prelaza. Levi prelaz, označen sa 'network error', povezuje se sa čvorom označenim sa 'error'. Desni prelaz, označen sa 'network success', povezuje se sa čvorom označenim sa 'success'.
Dijagram toka od leva ka desno sa 5 čvorova. Prvi čvor, označen sa 'empty', ima jedan prelaz, označen sa 'start typing', koji se povezuje sa čvorom označenim sa 'typing'. Taj čvor ima jedan prelaz, označen kao 'press submit', koji se povezuje sa čvorom koji je označen kao 'submitting', koji ima dva prelaza. Levi prelaz, označen sa 'network error', povezuje se sa čvorom označenim sa 'error'. Desni prelaz, označen sa 'network success', povezuje se sa čvorom označenim sa 'success'.

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 i isSubmitting ne mogu zajedno biti true. 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 u status 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 i isTyping ne mogu biti true istovremeno. Ako ih napravite kao odvojene state promenljive rizikujete da ne budu sinhronizovane i pravite bug-ove. Srećom, možete ukloniti isEmpty i umesto toga proveriti answer.length === 0.
  • Da li istu informaciju možete dobiti inverzijom druge state promenljive? isError nije potreban jer možete proveriti error !== 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

Eliminisanje “nemogućih” stanja sa reducer-om

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:
    1. Identifikujte sva njena vizuelna stanja.
    2. Odredite ljudske i računarske pokretače promena stanja.
    3. Modelujte stanje sa useState.
    4. Uklonite neobavezne state-ove da izbegnete bug-ove i paradokse.
    5. 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>
  );
}