Redosled serijskih ažuriranja state-a

Postavljanje state promenljive će staviti novi render u red čekanja. Ali, ponekad želite izvršiti više operacija nad promenljivom pre zakazivanja narednog rendera. Da biste to uradili, potrebno je razumeti kako React batch-uje ažuriranja state-a.

Naučićete:

  • Šta je “batch-ovanje” i kako ga React koristi da procesira više ažuriranja state-a
  • Kako da primenite više ažuriranja u nizu na jednu promenljivu state-a

React batch-uje ažuriranja state-a

Očekivali biste da klikom na dugme “+3” brojač bude inkrementiran tri puta jer se setNumber(number + 1) poziva tri puta:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

Međutim, kao što se verovatno sećate iz prethodne sekcije, vrednosti state-a su fiksirane za svaki render, pa je vrednost za number u event handler-u prvog rendera uvek 0, nevezano od toga koliko puta pozovete setNumber(1):

setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);

Ali, ovde postoji još jedan faktor. React čeka da se sav kod u event handler-ima izvrši pre nego što procesira ažuriranja state-a. Zato se ponovni render jedino dešava nakon svih setNumber() poziva.

Ovo vas može podsetiti na konobara koji uzima porudžbinu u restoranu. Konobar ne trči u kuhinju nakon što pomenete prvo jelo! Umesto toga, dopušta vam da završite porudžbinu, da je promenite, a čak uzima i porudžbine od drugih ljudi za stolom.

Elegantni kursor u restoranu poručuje više puta pomoću React-a, koji igra ulogu konobara. Nakon što više puta pozove setState(), konobar upisuje poslednje što je poručeno, kao konačnu porudžbinu.

Illustrated by Rachel Lee Nabors

Ovo vam omogućava da ažurirate više state promenljivih—čak i iz različitih komponenata—bez okidanja previše ponovnih rendera. Ali, to takođe znači da UI neće biti ažuriran sve dok se event handler i sav kod unutra ne izvrši. Ovo ponašanje, poznatije kao batch-ovanje, čini vašu React aplikaciju mnogo bržom. Takođe, izbegava se obrada zbunjujućih “polu-završenih” rendera gde su samo neke promenljive ažurirane.

React ne radi batch-ovanje više namernih event-ova poput klikova—svaki klik se obrađuje zasebno. Budite uvereni da React radi batch-ovanje samo kada je to sigurno. Ovo osigurava da se, na primer, ako prvi klik dugmeta onemogućuje formu, spreči da drugi klik uradi submit ponovo.

Ažuriranje istog state-a više puta pre narednog rendera

Ovo je neuobičajen slučaj, ali ako biste želeli da ažurirate istu state promenljivu više puta pre narednog rendera, umesto da prosledite narednu state vrednost poput setNumber(number + 1), možete proslediti funkciju koja računa naredni state na osnovu prethodnog u redu čekanja, poput setNumber(n => n + 1). Ovo je način da kažete React-u da “uradi nešto sa vrednosti state-a” umesto da je samo zameni.

Probajte sada da inkrementirate brojač:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(n => n + 1);
        setNumber(n => n + 1);
        setNumber(n => n + 1);
      }}>+3</button>
    </>
  )
}

Ovde, n => n + 1 se naziva updater funkcija. Kada je prosledite u setter za state:

  1. React postavlja ovu funkciju u red čekanja da bi bila procesirana nakon što se sav ostali kod u event handler-u izvrši.
  2. Tokom narednog rendera, React prolazi kroz red čekanja i daje vam konačni ažurirani state.
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

Evo kako React tumači ove linije koda dok izvršava event handler:

  1. setNumber(n => n + 1): n => n + 1 je funkcija. React je dodaje u red čekanja.
  2. setNumber(n => n + 1): n => n + 1 je funkcija. React je dodaje u red čekanja.
  3. setNumber(n => n + 1): n => n + 1 je funkcija. React je dodaje u red čekanja.

Kada se pozove useState tokom narednog rendera, React prolazi kroz red čekanja. Prethodni number state je bio 0, pa je to ono što React prosleđuje u prvu updater funkciju kao argument n. Nakon toga, React uzima povratnu vrednost prethodne updater funkcije i prosleđuje je u narednu updater funkciju kao n, i tako dalje:

ažuriranje u redu čekanjanpovratna vrednost
n => n + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 1 = 3

React čuva 3 kao konačan rezultat i vraća ga iz useState.

Zato se, klikom na “+3” u primeru iznad, vrednost ispravno inkrementira za 3.

Šta se dešava ako ažurirate state nakon što ga zamenite

Šta je sa ovim event handler-om? Šta mislite da će number biti u narednom renderu?

<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
      }}>Povećaj broj</button>
    </>
  )
}

Evo šta ovaj event handler govori React-u da uradi:

  1. setNumber(number + 5): number je 0, znači setNumber(0 + 5). React dodaje “zameni sa 5 u red čekanja.
  2. setNumber(n => n + 1): n => n + 1 je updater funkcija. React dodaje tu funkciju u red čekanja.

Tokom narednog rendera React prolazi kroz red čekanja za state:

ažuriranje u redu čekanjanpovratna vrednost
”zameni sa 50 (neupotrebljeno)5
n => n + 155 + 1 = 6

React čuva 6 kao konačan rezultat i vraća ga iz useState.

Napomena

Možda ste primetili da setState(5) zapravo radi kao setState(n => 5), ali n nije upotrebljeno!

Šta se dešava ako zamenite state nakon što ga ažurirate

Hajde da probamo još jedan primer. Šta mislite da će number biti u narednom renderu?

<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
        setNumber(42);
      }}>Povećaj broj</button>
    </>
  )
}

Evo kako React tumači ove linije koda dok izvršava event handler:

  1. setNumber(number + 5): number je 0, znači setNumber(0 + 5). React dodaje “zameni sa 5 u red čekanja.
  2. setNumber(n => n + 1): n => n + 1 je updater funkcija. React dodaje tu funkciju u red čekanja.
  3. setNumber(42): React dodaje “zameni sa 42 u red čekanja.

Tokom narednog rendera React prolazi kroz red čekanja za state:

ažuriranje u redu čekanjanpovratna vrednost
”zameni sa 50 (neupotrebljeno)5
n => n + 155 + 1 = 6
”zameni sa 426 (neupotrebljeno)42

React čuva 42 kao konačan rezultat i vraća ga iz useState.

Da sumiramo, evo kako možete razmišljati o onome što prosleđujete u setNumber setter za state:

  • Updater funkcija (npr. n => n + 1) se dodaje u red čekanja.
  • Bilo koja druga vrednost (npr. broj 5) dodaje “zameni sa 5” u red čekanja, ignorišući ono što je već u tom redu.

Kada se event handler izvrši, React će pokrenuti ponovni render. Tokom ponovnog rendera, React će obraditi red čekanja. Updater funkcije se izvršavaju tokom renderovanja, što znači da updater funkcije moraju biti čiste i da samo vrate rezultat. Nemojte pokušavati da postavite state unutar njih ili da izvršite druge propratne efekte. U Strict Mode-u, React će izvršiti svaku updater funkciju dvaput (ali će odbaciti rezultat druge) kako bi vam pomogao da pronađete greške.

Konvencije imenovanja

Uobičajeno je nazvati argument updater funkcije po prvim slovima odgovarajuće state promenljive:

setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);

Ako preferirate opširniji kod, druga konvencija je ponavljanje punog imena state promenljive, poput setEnabled(enabled => !enabled), ili da koristite prefiks, npr. setEnabled(prevEnabled => !prevEnabled).

Recap

  • Postavljanje state-a ne menja promenljivu u trenutnom renderu, ali zahteva novi render.
  • React procesira ažuriranje state-a nakon što su se event handler-i izvršili. Ovo se naziva batch-ovanje.
  • Da biste ažurirali neki state više puta u jednom event-u, možete koristiti setNumber(n => n + 1) updater funkciju.

Izazov 1 od 2:
Popraviti broj zahteva

Radite na aplikaciji za prodaju umetnosti koja omogućava korisniku da zatraži više porudžbina umetničkih predmeta istovremeno. Svaki put kada korisnik pritisne dugme “Kupi”, brojač “Na čekanju” treba da se poveća za jedan. Nakon tri sekunde, brojač “Na čekanju” treba da se smanji, a brojač “Završeno” da se uveća.

Međutim, brojač “Na čekanju” se ne ponaša kao što bi trebalo. Nakon što kliknete “Kupi”, smanji se na -1 (što ne bi trebalo da je moguće!). Ako kliknete brzo dvaput, deluje da se oba brojača ponašaju nepredvidivo.

Zašto se ovo dešava? Popravite oba brojača.

import { useState } from 'react';

export default function RequestTracker() {
  const [pending, setPending] = useState(0);
  const [completed, setCompleted] = useState(0);

  async function handleClick() {
    setPending(pending + 1);
    await delay(3000);
    setPending(pending - 1);
    setCompleted(completed + 1);
  }

  return (
    <>
      <h3>
        Na čekanju: {pending}
      </h3>
      <h3>
        Završeno: {completed}
      </h3>
      <button onClick={handleClick}>
        Kupi     
      </button>
    </>
  );
}

function delay(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
}