Tilapäivitysten lisääminen jonoon

Tilamuuttujan asettaminen lisää toisen renderöinnin jonoon. Toisinaan saatat haluta suorittaa useita operaatioita arvolla ennen seuraavan renderöinnin lisäämistä jonoon. Tätä varten on hyvä ymmärtää, miten React erittelee tilapäivitykset.

Tulet oppimaan

  • Mitä “niputtaminen” on ja kuinka React käyttää sitä prosessoidessaan useita tilapäivityksiä
  • Useiden päivitysten soveltaminen samaan tilamuuttujaan peräkkäin

React niputtaa tilapäivitykset

Saatat olettaa, että painamalla “+3”-painiketta laskurin lukuu kasvaa kolme kertaa, koska se kutsuu setNumber(number + 1) kolmesti:

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>
    </>
  )
}

Kuitenkin saatat muistaa edellisestä osasta, kunkin renderin tila-arvot ovat kiinteät, joten number muuttujan arvo ensimmäisen renderöinnin tapahtumakäsittelijässä on aina 0, riippumatta siitä miten monesti kutsut setNumber(1) funktiota:

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

Mutta tässä on yksi toinen tekijä mukana pelissä. React odottaa kunnes kaikki koodi tapahtumakäsittelijässä on suoritettu ennen tilapäivitysten laskentaa. Tämän vuoksi uudelleen renderöinti tapahtuu kaikkien setNumber() kutsujen jälkeen.

Tämä saattaa muistuttaa tarjoilijasta ottamassa tilauksia vastaan ravintolassa. Tarjoilija ei juokse keittiöön heti kun mainitset ensimmäisen aterian. Sen sijaan hän antaa sinun tehdä tilauksesi loppuun asti, tehdä siihen muutoksia ja sitten ottaa tilauksia vastaan muilta henkilöiltä pöydässä.

An elegant cursor at a restaurant places and order multiple times with React, playing the part of the waiter. After she calls setState() multiple times, the waiter writes down the last one she requested as her final order.

Illustrated by Rachel Lee Nabors

Tämän avulla voit päivittää useita tilamuuttujia—jopa useista komponenteista—ilman ylimääräisten renderöintien käynnistämistä. Tämä tarkoittaa kuitenkin myös sitä, että käyttöliittymä päivittyy vasta jälkeen, kun tapahtumankäsittelijäsi ja siinä oleva koodi on suoritettu. Tämä käyttäytyminen, joka tunnetaan myös nimellä niputtaminen (engl. batching), ja se saa React-sovelluksesi toimimaan paljon nopeammin. Sen avulla vältetään myös sekavat “puolivalmiit” renderöinnit, joissa vain osa muuttujista on päivitetty.

React ei niputa useita tarkoituksellisia tapahtumia kuten klikkauksia—jokainen klikkaus käsitellään erikseen. Voit olla varma, että React tekee niputtamista vain silloin, kun se on yleisesti ottaen turvallista. Näin varmistetaan, että jos esimerkiksi ensimmäinen painikkeen napsautus poistaa lomakkeen käytöstä, toinen napsautus ei lähetä lomaketta uudelleen.

Saman tilamuuttujan päivittäminen useita kertoja ennen seuraavaa renderöintiä

Tämä on harvinainen käyttötapaus, mutta jos haluat päivittää saman tilamuuttujan useita kertoja ennen seuraavaa renderöintiä, sen sijaan, että välittäisit seuraavan tilan arvon kuten setNumber(number + 1), voit välittää funktion, joka laskee seuraavan tilan jonon edellisen tilan perusteella, kuten setNumber(n => n + 1). Se on tapa käskeä Reactia “tekemään jotain tila-arvolla” sen sijaan, että se vain korvaisi sen.

Kokeile kasvattaa laskuria nyt:

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>
    </>
  )
}

Tässä, n => n + 1 onpäivitysfunktio. Kun välität sen tila-asettajalle:

  1. React lisää tämän funktion jonoon prosessoitavaksi kun kaikki muut koodi tapahtumakäsittelijässä on suoritettu.
  2. Seuraavan renderöinnin aikana React käy jonon läpi ja antaa sinulle lopullisen päivitetyn tilan.
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

Näin React käy läpi nämä rivit koodia tapahtumakäsittelijää suoritettaessa:

  1. setNumber(n => n + 1): n => n + 1 on funktio. React lisää sen jonoon.
  2. setNumber(n => n + 1): n => n + 1 on funktio. React lisää sen jonoon.
  3. setNumber(n => n + 1): n => n + 1 on funktio. React lisää sen jonoon.

Kun kutsut useState funktiota renderöinnin aikana, React käy jonon läpi. Edellinen number:n tila oli 0, joten React välittää sen ensimmäiselle päivitysfunktiolle argumenttina n. Sitten React ottaa edellisen päivitysfunktion paluuarvon ja siirtää sen seuraavalle päivitysfunktiolle n muuttujana, ja niin edelleen:

päivitys jonossanpalauttaa
n => n + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 1 = 3

React tallentaa 3 lopulliseksi tulokseksi ja palauttaa sen useState:sta.

Tämän vuoksi napsauttamalla “+3” yllä olevassa esimerkissä arvo kasvaa oikein 3:lla.

Mitä tapahtuu, jos päivität tilan sen korvaamisen jälkeen?

Entä tämä tapahtumankäsittelijä? Mitä luulet, että number on seuraavassa renderöinnissä?

<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);
      }}>Increase the number</button>
    </>
  )
}

Tämä tapahtumankäsittelijä käskee Reactia tekemään seuraavaa:

  1. setNumber(number + 5): number on 0, joten setNumber(0 + 5). React lisää “korvaa arvolla 5 sen jonoon.
  2. setNumber(n => n + 1): n => n + 1 on päivitysfunktio. React lisää tuon funktion sen jonoon.

Seuraavan renderöinnin aikana React käy läpi tilajonon:

päivitys jonossanpalauttaa
“replace with 50 (käyttämätön)5
n => n + 155 + 1 = 6

React tallentaa 6 lopulliseksi tulokseksi ja palauttaa sen useState:sta.

Huomaa

Olet ehkä huomannut, että setState(5) toimii kuten setState(n => 5), mutta n on käyttämätön!

Mitä tapahtuu, jos korvaat tilan sen päivittämisen jälkeen?

Kokeillaan vielä yhtä esimerkkiä. Mitä luulet, että number on seuraavassa renderöinnissä?

<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);
      }}>Increase the number</button>
    </>
  )
}

Näin React käy läpi nämä rivit koodia tapahtumakäsittelijää suoritettaessa:

  1. setNumber(number + 5): number on 0, joten setNumber(0 + 5). React lisää “korvaa arvolla 5 sen jonoon.
  2. setNumber(n => n + 1): n => n + 1 on päivitysfunktio. React lisää tuon funktion sen jonoon.
  3. setNumber(42): React lisää “korvaa arvolla 42 sen jonoon.

Seuraavan renderöinnin aikana React käy läpi tilajonon:

päivitys jonossanpalauttaa
“korvaa arvolla 50 (käyttämätön)5
n => n + 155 + 1 = 6
“korvaa arvolla 426 (käyttämätön)42

Sitten React tallentaa 42 lopulliseksi tulokseksi ja palauttaa sen useState:sta.

Yhteenvetona voit ajatella näin, mitä välität setNumber tila-asettimeen:

  • Päivitysfunktion (esim. n => n + 1) lisätään jonoon.
  • Minkä tahansa arvon (esim. numero 5) lisää “korvaa arvolla 5” jonoon, huomioimatta sitä, mikä on jo jonossa.

Tapahtumankäsittelijän päätyttyä React käynnistää uuden renderöinnin. Uudelleen renderöinnin aikana React käsittelee jonon. Päivitysfunktiot suoritetaan renderöinnin aikana, joten päivitysfunktioiden on oltava puhtaita ja palauttavat vain tuloksen. Älä yritä asettaa tilaa niiden sisältä tai suorittaa muita sivuvaikutuksia. Strict Modessa, React suorittaa jokaisen päivitysfunktion kahdesti (mutta hylkää toisen tuloksen) auttaakseen sinua löytämään virheitä.

Nimeämiskäytännöt

On tavallista nimetä päivitysfunktion argumentti vastaavan tilamuuttujan alkukirjaimilla:

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

Jos haluat yksityiskohtaisempaa koodia, toinen yleinen käytäntö on toistaa koko tilamuuttujan nimi, kuten setEnabled(enabled => !enabled), tai käyttää etuliitettä kuten setEnabled(prevEnabled => !prevEnabled).

Kertaus

  • Tilan asettaminen ei muuta tilamuuttujaa olemassa olevassa renderöinnissä, vaan pyytää uutta renderöintiä.
  • React käsittelee tilapäivitykset sen jälkeen, kun tapahtumakäsittelijät ovat lopettaneet suorituksensa. Tätä kutsutaan niputtamiseksi.
  • Jos haluat päivittää jonkin tilan useita kertoja yhdessä tapahtumassa, voit käyttää setNumber(n => n + 1)-päivitysfunktiota.

Haaste 1 / 2:
Korjaa pyyntöjen laskuri

Olet kehittämässä taiteen markkinapaikkasovellusta, jonka avulla käyttäjä voi tehdä useita tilauksia taide-esineestä samanaikaisesti. Joka kertan, kun käyttäjä painaa “Osta”-painiketta, “Vireillä”-laskurin pitäisi kasvaa yhdellä. Kolmen sekuntin kuluttua “Vireillä”-laskurin pitäisi pienentyä ja “Toteutunut” laskurin pitäisi kasvaaa.

Vireillä -laskuri ei kuitenkaan käyttäydy tarkoitetulla tavalla. Kun painat “Osta”, se laskee arvoon -1 (minkä ei pitäisi olla mahdollista!). Ja jos painat nopeasti kahdesti, molemmat laskurit näyttävät käyttäytyvän arvaamattomasti.

Miksi näin tapahtuu? Korjaa molemmat laskurit.

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>
        Vireillä: {pending}
      </h3>
      <h3>
        Toteutunut: {completed}
      </h3>
      <button onClick={handleClick}>
        Osta
      </button>
    </>
  );
}

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