Skip to main content

什麼時候不用批次更新?

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 輸出。

操作步驟:

  1. 建立一個新的 React 元件檔案(例如 Counter.js)。

  2. 複製上面的程式碼到檔案中。

  3. 在你的 React 專案中匯入並使用這個元件(例如在 App.js 中)。

  4. 執行專案,點擊按鈕,觀察控制台的輸出,確認 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。

操作步驟:

  1. 建立一個新的 React 元件檔案(例如 NativeEventCounter.js)。

  2. 複製上面的程式碼到檔案中。

  3. 在 App.js 中匯入並使用這個元件。

  4. 執行專案,點擊按鈕,觀察控制台輸出,確認兩次更新是分開進行的。

注意:

  • 這個範例中,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 值會正確反映。

操作步驟:

  1. 建立一個新的 React 元件檔案(例如 ImmediateUpdateCounter.js)。

  2. 確保你的專案已安裝 react-dom(通常 React 專案已包含)。

  3. 複製上面的程式碼到檔案中。

  4. 在 App.js 中匯入並使用這個元件。

  5. 執行專案,點擊按鈕,觀察控制台輸出,確認 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 可能會批次處理。

操作步驟:

  1. 確保你有一個 Next.js 專案(可使用 npx create-next-app 建立)。

  2. 將上面的程式碼放入 pages/index.js。

  3. 執行專案(npm run dev),點擊按鈕,觀察行為。

  4. 注意:需要在伺服器端環境中測試(例如 getServerSideProps),客戶端行為可能不同。

如何確認是否需要避免批次更新?

如果你不確定是否需要避免批次更新,可以問自己以下問題:

  1. 是否需要在更新後立即取得最新狀態? 如果是,使用 flushSync 或 useEffect 來處理。

  2. 是否在非同步程式碼中更新狀態? 如果是,React 預設不會批次處理,你可能需要手動管理更新邏輯。

  3. 是否使用原生 DOM 事件? 如果是,考慮改用 React 的事件系統(例如 onClick)來利用批次更新。

  4. 是否在伺服器端環境? 如果是,批次更新不會發生,需注意伺服器端行為。