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.

Memperbarui state yang sama beberapa kali sebelum render selanjutnya

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.

Ini bukanlah penggunaan yang umum, tetapi jika Anda ingin memperbarui variabel state yang sama berulang kali sebelum render selanjutnya, alih-alih mengoper nilai state selanjutnya seperti setNumber(number + 1), Anda dapat mengoper function yang menghitung state selanjutnya berdasarkan nilai sebelumnya pada antrean, seperti setNumber(n => n + 1). Ini adalah cara untuk memberi tahu React untuk “melakukan sesuatu dengan nilai state” daripada hanya menggantinya.

Cobalah untuk menambahkan hitungan sekarang:

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

Disini, n => n + 1 disebut fungsi updater. Ketika Anda mengirimkannya ke pengatur (setter) state:

  1. React mengantre fungsi ini untuk diproses setelah semua kode lain dalam event handler dijalankan.
  2. Saat render berikutnya, React akan melewati antrean dan memberi Anda state terakhir yang diperbarui.
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

Berikut adalah cara kerja React melalui baris kode ini saat menjalankan 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.

Ketika Anda memanggil useState saat render berikutnya, React akan melewati antrean. State number sebelumnya adalah 0, jadi itulah yang akan diteruskan React ke fungsi updater pertama sebagai argumen n. Kemudian React mengambil hasil dari fungsi updater sebelumnya dan meneruskannya ke updater berikutnya sebagai n, dan begitu seterusnya:

antrean diperbaruinhasil
n => n + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 1 = 3

React menyimpan 3 sebagai hasil akhir dan mengembalikannya dari useState.

Inila mengapa mengklik “+3” pada contoh di atas dengan benar meningkatkan nilai sebesar 3.

Apa yang terjadi jika Anda memperbarui state setelah menggantinya

Bagaimana dengan event handler ini? Menurut Anda berapa nilai number pada render berikutnya?

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

Begini cara event handler memberitahu React apa yang harus dilakukan:

  1. setNumber(number + 5): number adalah 0 maka setNumber(0 + 5). React menambahkan “ganti dengan 5” ke antreannya.
  2. setNumber(n => n + 1): n => n + 1 merupakan fungsi updater. React menambahkan fungsi tersebut ke antreannya.

Selama render berikutnya, React melewati antrean state:

antrean diperbaruinhasil
“ganti dengan 50 (tak terpakai)5
n => n + 155 + 1 = 6

React menyimpan 6 sebagai hasil akhir dan mengembalikannya dari useState.

Note

Anda mungkin sadar bahwa setState(5) sebenarnya bekerja seperti setState(n => 5), tetapi n tidak terpakai!

Apa yang terjadi jika Anda mengganti state setelah memperbaruinya

Mari kita coba satu contoh lagi. Menurut Anda berapa nilai number pada render berikutnya?

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

Begini cara React bekerja melalui baris kode ini saat menjalankan event handler:

  1. setNumber(number + 5) : numberadalah0makasetNumber(0 + 5). React menambahkan "ganti dengan 5`” ke antreannya.
  2. setNumber(n => n + 1): n => n + 1 adalah fungsi updater. React menambahkan fungsi tersebut ke antreannya.
  3. setNumber(42): React menambahkan “ganti dengan 42” ke antreannya.

Selama render berikutnya, React melewati antrean state:

antrean diperbaruinhasil
“ganti dengan 50 (tak terpakai)5
n => n + 155 + 1 = 6
“ganti dengan 426 (tak terpakai)42

Akibatnya, React menyiapkan 42 sebagai hasil akhir dan mengembalikannya dari useState.

Jadi, kesimpulannya adalah berikut cara Anda dapat memikirkan apa yang anda oper ke pengatur state setNumber:

  • Sebuah fungsi updater (misalnya n => n + 1) ditambahkan ke antrean.
  • Apapun nilai lainnya (misalnya angka 5) menambahkan “ganti dengan 5” ke antrean, mengabaikan apa yang sudah ada di antrean.

Setelah event handler selesai, React akan memicu re-render. Selama re-render, React akan memproses antrean. Fungsi updater berjalan selama proses render, jadi fungsi updater harus murni dan hanya mengembalikan hasilnya. Jangan mencoba mengatur state dari dalamnya atau menjalankan efek samping lainnya. Dalam Strict Mode, React akan menjalankan setiap fungsi updater dua kali (tetapi membuang hasil kedua) untuk membantu Anda menemukan kesalahan.

Konvensi penamaan

Seringkali nama fungsi updater diambil dari huruf pertama variabel state yang sesuai:

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

Jika Anda lebih suka kode yang lebih panjang, konvensi umum lainnya adalah mengulangi nama variabel state lengkap, seperti setEnabled(enabled => !enabled), atau menggunakan awalan seperti setEnabled(prevEnabled => !prevEnabled).

Recap

  • Mengatur state tidak mengubah variabel dalam render yang sudah ada, tetapi meminta render baru.
  • React memproses pembaruan state setelah event handler selesai berjalan. Ini disebut pengelompokan.
  • untuk memperbarui beberapa state beberapa kali dalam satu event, Anda dapat menggunakan fungsi updater setNumber(n => n + 1).

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