設置 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()
之後發生。
這可能會讓你想起餐廳的服務生點菜。服務生不會在你點第一道菜時就跑到廚房!相反,他們會讓你一次點完餐,可以讓你進行更改,甚至接受餐桌上其他人的點餐。
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 設置器時:
- React 將此函數加入隊列,以便在事件處理器程序的所有其他代碼運行後進行處理。
- 在下一次 Render 期間,React 會遍歷隊列並為你提供最終的更新 state。
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 將其添加到隊列中。
當你在下一次 render 期間呼叫 useState
,React 會遍歷隊列。前一個 number
的 state 是 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 從 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 要做的事情:
setNumber(number + 5)
:number
是0
,所以setNumber(0 + 5)
。React 將 「替換為5
」 添加到其隊列中。setNumber(n => n + 1)
:n => n + 1
是一個更新函數。React 將該函數添加到其隊列中。
在下一次 render 期間,React 會遍歷隊列的 state:
更新隊列 | n | 回傳 |
---|---|---|
「替換為 5 」 | 0 (未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
React 從 useState
存儲 6
作為最終結果。
如果在替換後更新 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 在執行此事件處理程序時會如何處理這些程式碼:
setNumber(number + 5)
:number
是0
,所以setNumber(0 + 5)
。React 將 「替換為5
」 添加到其隊列中。setNumber(n => n + 1)
:n => n + 1
是一個更新函數。React 將該函數添加到其隊列中。setNumber(42)
:React 將 「替換為42
」 添加到其隊列中。
在下一次 render 期間,React 會遍歷隊列的 state:
更新對列 | n | 回傳 |
---|---|---|
「替換為 5 」 | 0 (未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
「替換為 42 」 | 6 (未使用) | 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); }); }