將一系列的 State 更新加入隊列

設置 state 變數將使另一個 render 加入隊列。但有時後你可能希望在加入隊列之前對該變數進行多個操作。為此,了解 React 如何批次更新 state 會有所幫助。

You will learn

  • 什麼是「批次處理」以及 React 如何使用它來處理多個 state 更新
  • 如何連續對同一個 state 變數進行多次更新

React 批次更新 state

你可能預期點擊「+3」按鈕將增加計數三次,因為它調用了 setNumber(number + 1) 三次:

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

但是,正如你可能還記得上一節中所提到, 每個 render 的 state 值都是固定的,因此在第一個 render 事件處理程序的 number 值始終皆為0,無論呼叫多少次setNumber(1)

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

但這裡還有另一個原因。React 會等到所有在事件處理程序中的程式碼都運行完畢後才更新 state。 這就是為什麼重新 render 只有在呼叫所有setNumber()之後發生。

這可能會讓你想起餐廳的服務生點菜。服務生不會在你點第一道菜時就跑到廚房!相反,他們會讓你一次點完餐,可以讓你進行更改,甚至接受餐桌上其他人的點餐。

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

這使你可以更新多個 state 變數——甚至可以從多個 component 進行更新——而不會觸發太多重新 render。 這也意味著在事件處理程序及其中的任何程式碼執行完成之前, UI 不會進行更新。這種行為也稱為批次處理,使你的 React 應用程式執行得更快。它還避免了處理令人困惑的「半完成」render,也就是只更新了一些變數。

React 不會批次處理多個主動事件(例如點擊)——每次點擊都是單獨處理的。請放心,React 通常只在安全的情況下才進行批次處理。例如,如果第一次點擊按鈕禁用了表單,則第二次點擊將不會再次提交該表單。

在下一次 Render 之前多次更新相同的 state

這是一個不常見的範例,但如果你想在下一次 render 之前多次更新相同的 state 變數,像是setNumber(n => n + 1),可以傳遞一個函數,該函數根據前一個在隊列中的 state 來計算下一個 state,而不是像setNumber(number + 1)傳遞下一個 state 的值。這是一種告訴 React「用 state 值做某事」而不只是替換它的方法。

現在嘗試增加計數:

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

這裡的 n => n + 1 稱為更新函數。當你將其傳遞給 state 設置器時:

  1. React 將此函數加入隊列,以便在事件處理器程序的所有其他代碼運行後進行處理。
  2. 在下一次 Render 期間,React 會遍歷隊列並為你提供最終的更新 state。
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

以下是 React 在執行事件處理程序時如何處理這些程式碼:

  1. setNumber(n => n + 1): n => n + 1 是一個函數。React 將其添加到隊列中。
  2. setNumber(n => n + 1): n => n + 1 是一個函數。React 將其添加到隊列中。
  3. setNumber(n => n + 1): n => n + 1 是一個函數。React 將其添加到隊列中。

當你在下一次 render 期間呼叫 useState ,React 會遍歷隊列。前一個 number 的 state 是 0,所以這就是 React 傳遞給第一個更新函數作為 n 參數的內容。然後 React 會獲取前一個更新函數的回傳值並將其作為參數 n 傳遞給下一個更新函式,以此類推:

更新隊列n回傳
n => n + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 1 = 3

React 從 useState 存儲 3 最為最終的結果。

這就是為什麼在前面的案例中點擊「+3」會正確地將值增加 3。

如果在替換後更新 state 會發生什麼

這個事件處理程序如何?你認為下一個 render 時 number 會是什麼?

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

以下是這個事件處理程序告訴 React 要做的事情:

  1. setNumber(number + 5)number0,所以 setNumber(0 + 5)。React 將 「替換為5 添加到其隊列中。
  2. setNumber(n => n + 1)n => n + 1 是一個更新函數。React 將該函數添加到其隊列中。

在下一次 render 期間,React 會遍歷隊列的 state:

更新隊列n回傳
「替換為 50 (未使用)5
n => n + 155 + 1 = 6

React 從 useState 存儲 6 作為最終結果。

Note

你可能已經註意到,setState(5) 的運作原理實際上與 setState(n => 5) 類似,但未使用 n

如果在替換後更新 state 會發生什麼

讓我們再嘗試一個例子。你認為在下一個 render 時 number 會是什麼?

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

以下是 React 在執行此事件處理程序時會如何處理這些程式碼:

  1. setNumber(number + 5)number0,所以 setNumber(0 + 5)。React 將 「替換為 5 添加到其隊列中。
  2. setNumber(n => n + 1)n => n + 1是一個更新函數。React 將該函數添加到其隊列中。
  3. setNumber(42):React 將 「替換為 42 添加到其隊列中。

在下一次 render 期間,React 會遍歷隊列的 state:

更新對列n回傳
「替換為 50 (未使用)5
n => n + 155 + 1 = 6
「替換為 426 (未使用)42

然後 React 從 useState 將其存儲的 42 作為最終結果。

總而言之,以下是你如何考慮要傳遞給 setNumber 的 state 設置器的內容:

  • 更新函數 (例如 n => n + 1)被添加到隊列中。
  • 任何其他值 (例如 number 5)都會將「替換為 5」添加到隊列中,忽略已經在排隊的內容。

事件處理程序完成後,React 將觸發重新 render。在重新 render 期間,React 將處理隊列。更新函數在 render 期間運行,因此更新函數必須是純函數並且僅返回結果。不要嘗試從它們內部設置 state 或運行其他的副作用。在嚴格模式下,React 將運行每個更新函式兩次(但丟棄第二次的結果)以幫助你發現錯誤。

命名慣例

通常透過相應 state 變數的第一個字母來命名更新函式的參數:

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

如果你比較喜歡更詳細的命名,另一個常見慣例是重複完整的 state 變數名稱,例如 setEnabled(enabled => !enabled),或是使用前綴例如 setEnabled(prevEnabled => !prevEnabled)

Recap

  • 設置 state 不會更改現有 render 中的變數,但它會請求新的 render。
  • React 在執行完事件處理後所執行的 state 更新。稱為批次處理。
  • 要在一個事件中多次更新某個 state,可以使用 setNumber(n => n + 1) 更新函數。

Challenge 1 of 2:
修復請求計數器

你正在開發一個藝術品商店應用程式,該應用程式允許用戶同時提交一件藝術品的多個訂單。每次用戶按下「購買」按鈕,「待處理」計數器就會增加一。三秒後,「待處理」數量應減少,「已完成」數量應增加。

然而,「待處理」計數的行為並不如預期。當你按下「購買」時,它會減少到 -1(這應該是不可能的!)。如果你快速點擊兩次,兩個計數器的行為似乎都不可預測。

為什麼會出現這種情況?修復這兩個計數器。

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