設定狀態變數會將另一個渲染排入佇列。但有時您可能希望在將下一個渲染排入佇列之前對值執行多個操作。要做到這一點,了解 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()
呼叫*之後*發生。
這可能會讓您想起餐廳服務生點餐的場景。服務生不會在您提到第一道菜時就跑到廚房!相反,他們會讓您完成點餐,讓您進行更改,甚至接受同桌其他人的點餐。

圖示作者: 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
稱為 更新函式。當您將它傳遞給狀態設定器時
- React 會將此函式排入佇列,以便在事件處理程式中的所有其他程式碼執行完畢後再處理它。
- 在下一次渲染期間,React 會遍歷佇列並提供最終更新的狀態。
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
以下是 React 在執行事件處理程式時如何處理這些程式碼行的
setNumber(n => n + 1)
:n => n + 1
是一個函式。 React 將它新增到佇列中。setNumber(n => n + 1)
:n => n + 1
是一個函式。 React 將它新增到佇列中。setNumber(n => n + 1)
:n => n + 1
是一個函式。 React 將它新增到佇列中。
當您在下一次渲染期間呼叫 useState
時,React 會遍歷佇列。先前的 number
狀態為 0
,因此 React 將其作為 n
參數傳遞給第一個更新函式。然後 React 取得前一個更新函式的返回值,並將其作為 n
傳遞給下一個更新函式,依此類推
佇列更新 | n | 返回 |
---|---|---|
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 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 執行的操作
setNumber(number + 5)
:number
是0
,所以是setNumber(0 + 5)
。React 將_「替換為5
」_ 添加到其佇列中。setNumber(n => n + 1)
:n => n + 1
是一個更新函式。React 將_該函式_ 添加到其佇列中。
在下一次渲染期間,React 會遍歷狀態佇列
佇列更新 | n | 返回 |
---|---|---|
「替換為 5 」 | n => n + 1 :n 是 5 ,所以這個函式返回 5 + 1 = 6 。 | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
React 將 6
儲存為最終結果,並從 useState
返回它。
如果您在更新狀態後替換它,會發生什麼事?
讓我們再試一個例子。您認為在下一次渲染中 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 在執行此事件處理器時如何處理這些程式碼
setNumber(number + 5)
:number
是0
,所以是setNumber(0 + 5)
。React 將_「替換為5
」_ 添加到其佇列中。setNumber(n => n + 1)
:n => n + 1
是一個更新函式。React 將_該函式_ 添加到其佇列中。setNumber(n => n + 1)
:n => n + 1
是一個更新函式。React 將它添加到佇列中。
在下一次渲染期間,React 會遍歷狀態佇列
佇列更新 | n | 返回 |
---|---|---|
「替換為 5 」 | n => n + 1 :n 是 5 ,所以這個函式返回 5 + 1 = 6 。 | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
setNumber(42) :React 將_「替換為 42 」_ 添加到其佇列中。佇列中的前一個項目將被忽略。 | 「替換為 42 」 | 42 |
然後,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); }); }