React 18 的 effect 函式在 mount 時為何會執行兩次?
以下是針對你的問題「React 18 的 effect 函式在 mount 時為何會執行兩次?」以及「Reusable state」的詳細解答。我會以繁體中文、台灣慣用的表達方式,提供清晰且易於操作的教學,並包含完整的程式碼範例。因為你提到你的程式語言能力不強,我會盡量把步驟拆解得簡單易懂,並專注於 React 的語法和邏輯。
React 18 的 effect 函式在 mount 時為何會執行兩次?
在 React 18 中,當你使用 useEffect 函式時,若啟用了 Strict Mode(嚴格模式),你可能會觀察到 useEffect 在元件初次渲染(mount)時執行了兩次。這並不是一個 bug,而是 React 18 為了幫助開發者發現潛在問題而刻意設計的行為。以下是詳細的解釋和原因:
為什麼 useEffect 會執行兩次?
-
React 18 的 Strict Mode:
-
在 React 18 中,當你的應用程式運行在 Strict Mode 下時,React 會在開發模式(development mode)中模擬元件的 mount 和 unmount 行為。具體來說,React 會:
-
渲染元件(執行 useEffect 的 effect 函式)。
-
立即模擬解除掛載(執行 cleanup 函式,如果有設置)。
-
再次重新渲染元件(再次執行 useEffect 的 effect 函式)。
-
-
這種行為的目的是為了檢測你的程式碼是否正確處理了副作用(side effects)以及清理邏輯,確保在元件被卸載和重新掛載時不會出現問題。
-
-
模擬真實場景:
-
在真實的應用程式中,元件可能會因為狀態改變、路由切換或其他原因被卸載並重新掛載。React 18 的 Strict Mode 模擬這種行為,幫助你在開發階段發現問題,例如:
-
副作用是否正確清理(避免記憶體洩漏)。
-
副作用是否依賴不正確的狀態或變數,導致重複執行時出錯。
-
-
-
僅在開發模式發生:
-
這種「執行兩次」的行為只會在 開發模式(development mode) 下發生,且必須啟用 Strict Mode。
-
在 生產模式(production mode) 下,useEffect 只會執行一次,不會有這種行為。
-
-
如何確認是否為 Strict Mode 導致?
-
檢查你的 React 應用程式是否在 index.js 或 main.js 中使用了
<React.StrictMode>
包裝你的應用程式。例如:import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
); -
如果你移除
<React.StrictMode>
,useEffect 將只執行一次。
-
程式碼範例:觀察 useEffect 執行兩次
以下是一個簡單的範例,展示 useEffect 在 Strict Mode 下執行兩次的行為:
import React, { useEffect } from "react";
function MyComponent() {
useEffect(() => {
console.log("Effect 執行了!");
// Cleanup 函式(模擬元件卸載時的清理)
return () => {
console.log("Cleanup 執行了!");
};
}, []); // 空依賴陣列,表示只在 mount 和 unmount 時執行
return <div>這是一個元件</div>;
}
export default MyComponent;
運行結果(在 Strict Mode 下,開發模式): 控制台輸出會是:
Effect 執行了!
Cleanup 執行了!
Effect 執行了!
解釋:
-
第一次 Effect 執行了! 是因為元件初次渲染。
-
Cleanup 執行了! 是因為 React 模擬了元件的卸載。
-
第二次 Effect 執行了! 是因為 React 重新渲染了元件。
注意:
- 如果你有副作用(例如發送 API 請求、訂閱事件等),需要確保清理邏輯正確,避免重複執行導致問題。
如何應對 useEffect 執行兩次?
-
接受這是正常行為:
- 這是 React 18 Strict Mode 的設計,目的是幫助你寫出更穩定的程式碼。無需特別「修復」,因為這不會影響生產環境。
-
確保副作用是冪等的(Idempotent):
- 確保你的 useEffect 內的副作用即使執行兩次也不會造成問題。例如,如果你在 useEffect 中發送 API 請求,應該檢查是否已經發送過,或者在後端處理重複請求。
-
正確使用 Cleanup 函式:
-
確保你的 useEffect 包含清理邏輯。例如,如果你在 useEffect 中設置了一個計時器,應該在 cleanup 函式中清除它:
import React, { useEffect } from "react";
function TimerComponent() {
useEffect(() => {
const timer = setInterval(() => {
console.log("計時器運行中...");
}, 1000);
// Cleanup 函式
return () => {
clearInterval(timer);
console.log("計時器已清除");
};
}, []); // 空依賴陣列
return <div>計時器元件</div>;
}
export default TimerComponent;
-
-
如果不想看到執行兩次:
-
你可以選擇在開發環境中移除
<React.StrictMode>
,但這不是推薦的做法,因為 Strict Mode 能幫助你發現潛在問題。 -
或者,你可以在 useEffect 中加入條件檢查,避免不必要的副作用執行(但這應該謹慎使用)。
-
Reusable State
「Reusable state」指的是在 React 應用程式中,將狀態邏輯抽取出來,以便在多個元件中重複使用。這可以提高程式碼的可維護性和可重用性。React 提供了幾種方式來實現可重用的狀態邏輯,以下是詳細的教學和範例。
為什麼需要 Reusable State?
在 React 應用程式中,元件的狀態邏輯(例如處理表單輸入、計數器邏輯、API 資料獲取等)可能在多個元件中重複出現。為了避免重複撰寫相同的邏輯,我們可以將狀態邏輯封裝起來,讓它可以被多個元件共享。常見的方式包括:
-
自訂 Hook(Custom Hook)
-
高階元件(Higher-Order Component, HOC)
-
Context API
在 React 18 中,自訂 Hook 是最常見且推薦的方式,因為它簡單、靈活且符合 React 的函數式程式設計理念。
方式一:使用自訂 Hook 實現 Reusable State
自訂 Hook 是一種以 use 開頭的函數,封裝了狀態和相關邏輯,並返回狀態值和操作函數。以下是一個簡單的計數器範例,展示如何創建可重用的狀態邏輯:
步驟 1:創建自訂 Hook
// hooks/useCounter.js
import { useState } from "react";
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
export default useCounter;
說明:
-
這個 useCounter Hook 封裝了計數器的狀態(count)和操作(increment、decrement、reset)。
-
你可以傳入一個初始值(initialValue),預設為 0。
-
返回的是一個物件,包含當前計數和三個操作函數。
步驟 2:在元件中使用自訂 Hook
// components/Counter.js
import React from "react";
import useCounter from "./hooks/useCounter";
function Counter() {
const { count, increment, decrement, reset } = useCounter(0);
return (
<div>
<h1>計數器: {count}</h1>
<button onClick={increment}>增加</button>
<button onClick={decrement}>減少</button>
<button onClick={reset}>重置</button>
</div>
);
}
export default Counter;
步驟 3:在另一個元件中重用
// components/AnotherCounter.js
import React from "react";
import useCounter from "./hooks/useCounter";
function AnotherCounter() {
const { count, increment, decrement, reset } = useCounter(10); // 初始值設為 10
return (
<div>
<h1>另一個計數器: {count}</h1>
<button onClick={increment}>加一</button>
<button onClick={decrement}>減一</button>
<button onClick={reset}>回到初始值</button>
</div>
);
}
export default AnotherCounter;
運行結果:
-
Counter 元件和 AnotherCounter 元件各自獨立擁有自己的計數器狀態,但它們共享了 useCounter Hook 的邏輯。
-
每個元件的 count 是獨立的,不會互相影響。
優點:
-
程式碼簡潔,邏輯集中在 useCounter Hook 中。
-
可以在任何需要計數器邏輯的元件中重用。
-
易於測試和維護。
方式二:使用 Context API 實現 Reusable State
如果多個元件需要共享同一個狀態(例如全域主題、使用者資料等),可以使用 Context API 結合自訂 Hook 來實現可重用的狀態。
步驟 1:創建 Context 和 Provider
// context/ThemeContext.js
import React, { createContext, useContext, useState } from "react";
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
步驟 2:在應用程式中使用 Provider
// App.js
import React from "react";
import { ThemeProvider } from "./context/ThemeContext";
import ThemeToggle from "./components/ThemeToggle";
import DisplayTheme from "./components/DisplayTheme";
function App() {
return (
<ThemeProvider>
<div>
<h1>主題切換應用</h1>
<ThemeToggle />
<DisplayTheme />
</div>
</ThemeProvider>
);
}
export default App;
步驟 3:在元件中使用 useTheme Hook
// components/ThemeToggle.js
import React from "react";
import { useTheme } from "../context/ThemeContext";
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
切換到 {theme === "light" ? "暗黑模式" : "明亮模式"}
</button>
);
}
export default ThemeToggle;
// components/DisplayTheme.js
import React from "react";
import { useTheme } from "../context/ThemeContext";
function DisplayTheme() {
const { theme } = useTheme();
return (
<div
style={{
background: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#000" : "#fff",
}}
>
當前主題: {theme}
</div>
);
}
export default DisplayTheme;
運行結果:
-
點擊 ThemeToggle 元件的按鈕會切換主題(light/dark)。
-
DisplayTheme 元件會根據當前主題改變背景和文字顏色。
-
所有使用 useTheme Hook 的元件共享同一個主題狀態。
優點:
-
Context API 適合全域狀態管理,減少了 props 傳遞的複雜度。
-
useTheme Hook 讓狀態邏輯變得可重用且易於存取。
注意事項
-
選擇合適的方式:
-
如果狀態邏輯是局部的(例如計數器、表單輸入),使用自訂 Hook。
-
如果狀態需要跨多個元件共享(例如主題、使用者資料),使用 Context API 結合自訂 Hook。
-
-
避免過度複雜化:
-
自訂 Hook 應該保持簡單,專注於單一職責。
-
不要在一個 Hook 中放入過多邏輯,否則會難以維護。
-
-
效能考量:
-
在使用 Context API 時,確保只將必要的資料放入 Context,避免不必要的重新渲染。
-
使用 useMemo 或 useCallback 來優化複雜的計算或函數。
-
總結
-
React 18 的 useEffect 執行兩次:
-
這是 Strict Mode 在開發模式下的行為,模擬元件卸載和重新掛載。
-
解決方法是確保副作用和清理邏輯正確,接受這是正常行為,或在必要時移除 Strict Mode(不推薦)。
-
範例程式碼展示了如何正確撰寫 useEffect 和清理邏輯。
-
-
Reusable State:
-
使用自訂 Hook(如 useCounter)封裝局部的狀態邏輯,適合簡單、可重用的場景。
-
使用 Context API 結合自訂 Hook(如 useTheme)實現全域狀態管理,適合跨元件共享的狀態。
-
範例程式碼展示了如何實作這兩種方式,並確保程式碼簡潔且易於維護。
-