批量執行一系列狀態更新

設定狀態變數會將另一個渲染排入佇列。但有時您可能希望在將下一個渲染排入佇列之前對值執行多個操作。要做到這一點,了解 React 如何批量處理狀態更新會有所幫助。

你將學到

  • 什麼是「批量處理」,以及 React 如何使用它來處理多個狀態更新
  • 如何連續對同一個狀態變數應用多個更新

React 批量處理狀態更新

您可能會預期點擊「+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>
    </>
  )
}

然而,您可能記得在上一節中,每個渲染的狀態值是固定的,因此在第一個渲染的事件處理程式中, number 的值始終為 0,無論您呼叫 setNumber(1) 多少次

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

但這裡還有一個因素在起作用。React 會等到事件處理程式中的*所有*程式碼都執行完畢後,才會處理您的狀態更新。這就是為什麼重新渲染只在所有這些 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.

圖示作者: Rachel Lee Nabors

這讓您可以更新多個狀態變數——即使來自多個元件——而不會觸發過多的 重新渲染。但這也意味著 UI 在您的事件處理程式及其中的任何程式碼完成*之後*才會更新。這種行為,也稱為 批量處理,可以讓您的 React 應用程式執行得更快。它還可以避免處理令人困惑的「半成品」渲染,其中只有一部分變數已更新。

React 不會跨*多個*故意事件(例如點擊)進行批量處理——每個點擊都單獨處理。請放心,React 只會在通常安全的情況下進行批量處理。例如,這確保了如果第一次點擊按鈕停用了表單,則第二次點擊不會再次提交它。

在下一次渲染前多次更新同一個狀態

這是一個不常見的用例,但如果您想在下一次渲染前多次更新同一個狀態變數,而不是傳遞*下一個狀態值*,例如 setNumber(number + 1),您可以傳遞一個*函式*,該函式根據佇列中的前一個狀態計算下一個狀態,例如 setNumber(n => n + 1)。這是一種告訴 React「對狀態值執行某些操作」而不是僅僅替換它的方法。

現在嘗試遞增計數器

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 稱為 更新函式。當您將它傳遞給狀態設定器時

  1. React 會將此函式排入佇列,以便在事件處理程式中的所有其他程式碼執行完畢後再處理它。
  2. 在下一次渲染期間,React 會遍歷佇列並提供最終更新的狀態。
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 將它新增到佇列中。

當您在下一次渲染期間呼叫 useState 時,React 會遍歷佇列。先前的 number 狀態為 0,因此 React 將其作為 n 參數傳遞給第一個更新函式。然後 React 取得前一個更新函式的返回值,並將其作為 n 傳遞給下一個更新函式,依此類推

佇列更新n返回
n => n + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 1 = 3

React 將 3 儲存為最終結果,並從 useState 返回它。

這就是為什麼在上述範例中點擊「+3」會正確地將值遞增 3 的原因。

如果您在替換狀態後更新它,會發生什麼事?

那這個事件處理器呢?您認為在下一次渲染中 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 將_該函式_ 添加到其佇列中。

在下一次渲染期間,React 會遍歷狀態佇列

佇列更新n返回
「替換為 5n => n + 1n5,所以這個函式返回 5 + 1 = 65
n => n + 155 + 1 = 6

React 將 6 儲存為最終結果,並從 useState 返回它。

注意

您可能已經注意到,setState(5) 實際上像 setState(n => 5) 一樣工作,但 n 未被使用!

如果您在更新狀態後替換它,會發生什麼事?

讓我們再試一個例子。您認為在下一次渲染中 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(n => n + 1)n => n + 1 是一個更新函式。React 將它添加到佇列中。

在下一次渲染期間,React 會遍歷狀態佇列

佇列更新n返回
「替換為 5n => n + 1n5,所以這個函式返回 5 + 1 = 65
n => n + 155 + 1 = 6
setNumber(42):React 將_「替換為 42」_ 添加到其佇列中。佇列中的前一個項目將被忽略。「替換為 4242

然後,React 將 42 儲存為最終結果,並從 useState 返回它。

總之,以下是您可以如何理解您傳遞給 setNumber 狀態設定器的內容

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

事件處理器完成後,React 將觸發重新渲染。在重新渲染期間,React 將處理佇列。更新函式會在渲染期間運行,因此更新函式必須是純粹的,並且僅_返回_結果。不要嘗試從它們內部設定狀態或運行其他副作用。在嚴格模式下,React 將運行每個更新函式兩次(但捨棄第二次結果),以幫助您找到錯誤。

命名慣例

通常會使用對應狀態變數的首字母來命名更新函式參數

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

如果您喜歡更詳細的程式碼,另一個常見的慣例是重複完整的狀態變數名稱,例如 setEnabled(enabled => !enabled),或者使用前綴,例如 setEnabled(prevEnabled => !prevEnabled)

重點回顧

  • 設定狀態不會更改現有渲染中的變數,但它會請求新的渲染。
  • React 在事件處理器完成運行後處理狀態更新。這稱為批次處理。
  • 要在一個事件中多次更新某些狀態,您可以使用 setNumber(n => n + 1) 更新函式。

挑戰 1 2:
修復請求計數器

您正在開發一個藝術品交易平台應用程式,允許使用者同時提交多個藝術品訂單。每次使用者按下「購買」按鈕時,「待處理」計數器應增加 1。三秒後,「待處理」計數器應減少,而「已完成」計數器應增加。

但是,「待處理」計數器的行為不如預期。當您按下「購買」時,它會減少到 -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);
  });
}