Mengantre Serangkaian Pembaruan State

Mengatur variabel state akan menambahkan antrean (queue) render baru. Terkadang Anda ingin melakukan beberapa operasi terhadap nilai sebelum menambahkan antrean render selanjutnya. Untuk melakukannya, penting untuk memahami bagaimana React melakukan pembaruan state secara berkelompok.

You will learn

  • Apa itu “pengelompokan (batching)” dan bagaimana React menggunakannya untuk memproses beberapa pembaruan state
  • Bagaimana menerapkan beberapa pembaruan ke variabel state yang sama secara berurutan

Mengelompokkan pembaruan state dalam React

Anda mungkin berharap bahwa menekan tombol “+3” akan menambahkan hitungan tiga kali karena memanggil setNumber(number + 1) tiga kali:

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

Namun, seperti yang mungkin Anda ingat dari bagian sebelumnya, nilai state setiap render adalah tetap, sehingga nilai number di dalam event handler render pertama selalu 0, tidak peduli berapa kali Anda memanggil setNumber(1):


```js
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);

Akan tetapi, ada satu faktor lain yang berperan di sini. React menunggu sampai semua kode dalam event handler selesai dijalankan sebelum memproses pembaruan state Anda. Inilah sebabnya mengapa re-render hanya terjadi setelah semua setNumber() dipanggil.

Ini mungkin mengingatkan Anda pada seorang pelayan yang menerima pesanan di restoran. Seorang pelayan tidak berlari ke dapur saat Anda menyebutkan hidangan pertama Anda! Sebaliknya, mereka membiarkan Anda menyelesaikan pesanan Anda, membiarkan Anda mengubahnya, dan bahkan menerima pesanan dari orang lain di meja tersebut.

Sebuah kursor elegan di sebuah restoran memesan beberapa kali dengan React, memainkan peran pelayan. Setelah dia memanggil setState() beberapa kali, pelayan menulis yang terakhir yang dia minta sebagai pesanan akhirnya.

Illustrated by Rachel Lee Nabors

Ini memungkinkan Anda memperbarui beberapa variabel state—bahkan dari beberapa komponen—tanpa memicu terlalu banyak re-render. Akan tetapi, hal ini ini membuat UI tidak akan diperbarui hingga setelah event handler Anda, dan kode apa pun di dalamnya, selesai dijalankan. Perilaku ini, juga dikenal sebagai pengelompokan, membuat aplikasi React Anda berjalan lebih cepat. Ini juga menghindari penanganan render “setengah jadi” yang membingungkan ketika hanya beberapa variabel yang diperbarui.

React tidak melakukan pengelompokkan pada beberapa event yang disengaja, seperti klik—setiap klik ditangani secara terpisah. Pastikan bahwa React hanya melakukan pengelompokan ketika aman untuk dilakukan. Ini memastikan bahwa, misalnya, jika klik tombol pertama menonaktifkan form, klik kedua tidak akan mengirimkannya lagi.

Updating the same state multiple times before the next render

It is an uncommon use case, but if you would like to update the same state variable multiple times before the next render, instead of passing the next state value like setNumber(number + 1), you can pass a function that calculates the next state based on the previous one in the queue, like setNumber(n => n + 1). It is a way to tell React to “do something with the state value” instead of just replacing it.

Try incrementing the counter now:

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

Here, n => n + 1 is called an updater function. When you pass it to a state setter:

  1. React queues this function to be processed after all the other code in the event handler has run.
  2. During the next render, React goes through the queue and gives you the final updated state.
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

Here’s how React works through these lines of code while executing the event handler:

  1. setNumber(n => n + 1): n => n + 1 is a function. React adds it to a queue.
  2. setNumber(n => n + 1): n => n + 1 is a function. React adds it to a queue.
  3. setNumber(n => n + 1): n => n + 1 is a function. React adds it to a queue.

When you call useState during the next render, React goes through the queue. The previous number state was 0, so that’s what React passes to the first updater function as the n argument. Then React takes the return value of your previous updater function and passes it to the next updater as n, and so on:

queued updatenreturns
n => n + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 1 = 3

React stores 3 as the final result and returns it from useState.

This is why clicking “+3” in the above example correctly increments the value by 3.

What happens if you update state after replacing it

What about this event handler? What do you think number will be in the next render?

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

Here’s what this event handler tells React to do:

  1. setNumber(number + 5): number is 0, so setNumber(0 + 5). React adds “replace with 5 to its queue.
  2. setNumber(n => n + 1): n => n + 1 is an updater function. React adds that function to its queue.

During the next render, React goes through the state queue:

queued updatenreturns
“replace with 50 (unused)5
n => n + 155 + 1 = 6

React stores 6 as the final result and returns it from useState.

Note

You may have noticed that setState(5) actually works like setState(n => 5), but n is unused!

What happens if you replace state after updating it

Let’s try one more example. What do you think number will be in the next render?

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

Here’s how React works through these lines of code while executing this event handler:

  1. setNumber(number + 5): number is 0, so setNumber(0 + 5). React adds “replace with 5 to its queue.
  2. setNumber(n => n + 1): n => n + 1 is an updater function. React adds that function to its queue.
  3. setNumber(42): React adds “replace with 42 to its queue.

During the next render, React goes through the state queue:

queued updatenreturns
“replace with 50 (unused)5
n => n + 155 + 1 = 6
“replace with 426 (unused)42

Then React stores 42 as the final result and returns it from useState.

To summarize, here’s how you can think of what you’re passing to the setNumber state setter:

  • An updater function (e.g. n => n + 1) gets added to the queue.
  • Any other value (e.g. number 5) adds “replace with 5” to the queue, ignoring what’s already queued.

After the event handler completes, React will trigger a re-render. During the re-render, React will process the queue. Updater functions run during rendering, so updater functions must be pure and only return the result. Don’t try to set state from inside of them or run other side effects. In Strict Mode, React will run each updater function twice (but discard the second result) to help you find mistakes.

Naming conventions

It’s common to name the updater function argument by the first letters of the corresponding state variable:

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

If you prefer more verbose code, another common convention is to repeat the full state variable name, like setEnabled(enabled => !enabled), or to use a prefix like setEnabled(prevEnabled => !prevEnabled).

Recap

  • Setting state does not change the variable in the existing render, but it requests a new render.
  • React processes state updates after event handlers have finished running. This is called batching.
  • To update some state multiple times in one event, you can use setNumber(n => n + 1) updater function.

Challenge 1 of 2:
Fix a request counter

You’re working on an art marketplace app that lets the user submit multiple orders for an art item at the same time. Each time the user presses the “Buy” button, the “Pending” counter should increase by one. After three seconds, the “Pending” counter should decrease, and the “Completed” counter should increase.

However, the “Pending” counter does not behave as intended. When you press “Buy”, it decreases to -1 (which should not be possible!). And if you click fast twice, both counters seem to behave unpredictably.

Why does this happen? Fix both counters.

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>
        Pending: {pending}
      </h3>
      <h3>
        Completed: {completed}
      </h3>
      <button onClick={handleClick}>
        Buy     
      </button>
    </>
  );
}

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