什麼時候不用批次更新?
React 的批次更新通常在 事件處理函數(如 onClick、onChange 等)或 React 控制的生命週期方法中自動發生。但在以下情況下,React 不會進行批次更新,或者你可能需要手動避免批次更新:
1. 非同步操作(例如 setTimeout、Promise、或 async/await)
在非同步程式碼中,React 無法自動將狀態更新合併,因為這些更新不在 React 的事件處理流程中。這時每次呼叫 setState 都會立即觸發重新渲染。
2. 直接使用原生 DOM 事件
如果你繞過 React 的事件系統(例如使用 addEventListener 綁定事件),React 無法控制這些事件的狀態更新,因此不會進行批次更新。
3. 需要立即取得更新後的狀態
如果你需要在某次狀態更新後立即取得最新的狀態值(例如在更新後執行某些計算),你可能需要手動避免批次更新,或者使用 useEffect 來處理。
4. 在伺服器端渲染(SSR)或靜態生成(SSG)中
在伺服器端渲染或靜態生成時,React 不會進行批次更新,因為這些環境中沒有瀏覽器的事件循環。
5. 明確使用 flushSync 強制同步更新
React 提供了 flushSync 方法(從 react-dom 匯入),可以強制立即執行狀態更新並渲染,繞過批次更新機制。
詳細說明與程式碼範例
以下我會針對每個情況提供詳細的程式碼範例,說明什麼時候不用批次更新,並附上完整的 JavaScript 程式碼,讓你可以直接複製並跟著操作。
1. 非同步操作中的狀態更新
當你在 setTimeout、Promise 或 async/await 中更新狀態時,React 不會將多次 setState 合併為一次渲染,每次更新都會立即觸發重新渲染。
範例程式碼:
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// 在 setTimeout 中更新狀態,不會批次處理
setTimeout(() => {
setCount(count + 1); // 第一次更新
console.log("count after first update:", count);
setCount(count + 1); // 第二次更新
console.log("count after second update:", count);
}, 1000);
};
console.log("Render with count:", count);
return (
<div>
<p>計數器: {count}</p>
<button onClick={handleClick}>點我加兩次</button>
</div>
);
}
export default Counter;
說明:
-
在這個範例中,當你點擊按鈕後,setTimeout 會延遲 1 秒執行兩次 setCount。
-
由於這是在非同步的 setTimeout 中,React 不會將這兩次 setCount 合併為一次渲染。
-
結果是,React 會渲染兩次,count 會先從 0 變成 1,然後再變成 2。
-
你會在控制台看到兩次不同的 Render with count 輸出。
操作步驟:
-
建立一個新的 React 元件檔案(例如 Counter.js)。
-
複製上面的程式碼到檔案中。
-
在你的 React 專案中匯入並使用這個元件(例如在 App.js 中)。
-
執行專案,點擊按鈕,觀察控制台的輸出,確認 count 是分兩次更新的。
2. 使用原生 DOM 事件
如果你直接使用原生 DOM 事件(例如 addEventListener),React 無法控制這些事件,因此不會進行批次更新。
範例程式碼:
import React, { useState, useEffect } from "react";
function NativeEventCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const button = document.getElementById("native-button");
button.addEventListener("click", () => {
setCount(count + 1); // 第一次更新
console.log("count after first update:", count);
setCount(count + 1); // 第二次更新
console.log("count after second update:", count);
});
// 清除事件監聽器,避免記憶體洩漏
return () => {
button.removeEventListener("click");
};
}, [count]); // 注意:這裡的 count 依賴可能會導致多次綁定,僅為示範
console.log("Render with count:", count);
return (
<div>
<p>計數器: {count}</p>
<button id="native-button">使用原生事件加兩次</button>
</div>
);
}
export default NativeEventCounter;
說明:
-
這個範例使用 useEffect 來綁定原生 DOM 事件(click)。
-
當你點擊按鈕時,兩次 setCount 不會被批次處理,React 會分別執行兩次渲染。
-
控制台會顯示兩次不同的 Render with count 輸出,count 會逐步從 0 增加到 1,然後到 2。
操作步驟:
-
建立一個新的 React 元件檔案(例如 NativeEventCounter.js)。
-
複製上面的程式碼到檔案中。
-
在 App.js 中匯入並使用這個元件。
-
執行專案,點擊按鈕,觀察控制台輸出,確認兩次更新是分開進行的。
注意:
- 這個範例中,useEffect 的依賴陣列包含 count,可能會導致事件監聽器重複綁定。這裡僅為示範,實際開發中應避免這種情況(例如移除 count 依賴,或使用 useRef 來管理事件)。
3. 需要立即取得更新後的狀態
如果你需要在某次狀態更新後立即使用最新的狀態值,批次更新可能會導致問題,因為 setState 是非同步的。此時,你可以使用 useEffect 或 flushSync 來確保立即更新。
範例程式碼(使用 flushSync):
import React, { useState } from "react";
import { flushSync } from "react-dom";
function ImmediateUpdateCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
flushSync(() => {
setCount(count + 1); // 第一次更新,立即渲染
console.log("count after first update:", count);
});
console.log("count after flushSync:", count); // 這裡 count 已經更新
flushSync(() => {
setCount(count + 1); // 第二次更新,立即渲染
console.log("count after second update:", count);
});
};
console.log("Render with count:", count);
return (
<div>
<p>計數器: {count}</p>
<button onClick={handleClick}>立即加兩次</button>
</div>
);
}
export default ImmediateUpdateCounter;
說明:
-
這裡使用 flushSync 來強制 React 立即執行狀態更新並重新渲染。
-
每次 setCount 都會立即觸發渲染,因此 count 的值會在每次 flushSync 後正確更新。
-
控制台會顯示兩次渲染,且 count 值會正確反映。
操作步驟:
-
建立一個新的 React 元件檔案(例如 ImmediateUpdateCounter.js)。
-
確保你的專案已安裝 react-dom(通常 React 專案已包含)。
-
複製上面的程式碼到檔案中。
-
在 App.js 中匯入並使用這個元件。
-
執行專案,點擊按鈕,觀察控制台輸出,確認 count 是立即更新的。
注意:
- 使用 flushSync 會降低性能,因為它強制 React 立即渲染。請謹慎使用,僅在需要立即更新時才使用。
4. 伺服器端渲染(SSR)或靜態生成(SSG)
在伺服器端渲染或靜態生成中,React 不會進行批次更新,因為這些環境中沒有瀏覽器的事件循環。這部分比較進階,通常需要搭配如 Next.js 這樣的框架來測試。
範例程式碼(以 Next.js 為例):
// pages/index.js
import { useState } from "react";
export default function Home() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // 在 SSR 中,這不會被批次處理
console.log("count after first update:", count);
setCount(count + 1); // 會單獨觸發更新
console.log("count after second update:", count);
};
console.log("Render with count:", count);
return (
<div>
<p>計數器: {count}</p>
<button onClick={handleClick}>加兩次</button>
</div>
);
}
說明:
-
在 Next.js 的伺服器端渲染中,setState 不會被批次處理,因為伺服器端沒有 React 的事件系統。
-
這部分需要你有 Next.js 環境來測試。如果你在純客戶端渲染(CSR)中運行,React 可能會批次處理。
操作步驟:
-
確保你有一個 Next.js 專案(可使用 npx create-next-app 建立)。
-
將上面的程式碼放入 pages/index.js。
-
執行專案(npm run dev),點擊按鈕,觀察行為。
-
注意:需要在伺服器端環境中測試(例如 getServerSideProps),客戶端行為可能不同。
如何確認是否需要避免批次更新?
如果你不確定是否需要避免批次更新,可以問自己以下問題:
-
是否需要在更新後立即取得最新狀態? 如果是,使用 flushSync 或 useEffect 來處理。
-
是否在非同步程式碼中更新狀態? 如果是,React 預設不會批次處理,你可能需要手動管理更新邏輯。
-
是否使用原生 DOM 事件? 如果是,考慮改用 React 的事件系統(例如 onClick)來利用批次更新。
-
是否在伺服器端環境? 如果是,批次更新不會發生,需注意伺服器端行為。