Skip to main content

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 會執行兩次?

  1. React 18 的 Strict Mode

    • 在 React 18 中,當你的應用程式運行在 Strict Mode 下時,React 會在開發模式(development mode)中模擬元件的 mount 和 unmount 行為。具體來說,React 會:

      1. 渲染元件(執行 useEffect 的 effect 函式)。

      2. 立即模擬解除掛載(執行 cleanup 函式,如果有設置)。

      3. 再次重新渲染元件(再次執行 useEffect 的 effect 函式)。

    • 這種行為的目的是為了檢測你的程式碼是否正確處理了副作用(side effects)以及清理邏輯,確保在元件被卸載和重新掛載時不會出現問題。

  2. 模擬真實場景

    • 在真實的應用程式中,元件可能會因為狀態改變、路由切換或其他原因被卸載並重新掛載。React 18 的 Strict Mode 模擬這種行為,幫助你在開發階段發現問題,例如:

      • 副作用是否正確清理(避免記憶體洩漏)。

      • 副作用是否依賴不正確的狀態或變數,導致重複執行時出錯。

  3. 僅在開發模式發生

    • 這種「執行兩次」的行為只會在 開發模式(development mode) 下發生,且必須啟用 Strict Mode。

    • 生產模式(production mode) 下,useEffect 只會執行一次,不會有這種行為。

  4. 如何確認是否為 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 執行兩次?

  1. 接受這是正常行為

    • 這是 React 18 Strict Mode 的設計,目的是幫助你寫出更穩定的程式碼。無需特別「修復」,因為這不會影響生產環境。
  2. 確保副作用是冪等的(Idempotent)

    • 確保你的 useEffect 內的副作用即使執行兩次也不會造成問題。例如,如果你在 useEffect 中發送 API 請求,應該檢查是否已經發送過,或者在後端處理重複請求。
  3. 正確使用 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;
  4. 如果不想看到執行兩次

    • 你可以選擇在開發環境中移除 <React.StrictMode>,但這不是推薦的做法,因為 Strict Mode 能幫助你發現潛在問題。

    • 或者,你可以在 useEffect 中加入條件檢查,避免不必要的副作用執行(但這應該謹慎使用)。


Reusable State

「Reusable state」指的是在 React 應用程式中,將狀態邏輯抽取出來,以便在多個元件中重複使用。這可以提高程式碼的可維護性和可重用性。React 提供了幾種方式來實現可重用的狀態邏輯,以下是詳細的教學和範例。

為什麼需要 Reusable State?

在 React 應用程式中,元件的狀態邏輯(例如處理表單輸入、計數器邏輯、API 資料獲取等)可能在多個元件中重複出現。為了避免重複撰寫相同的邏輯,我們可以將狀態邏輯封裝起來,讓它可以被多個元件共享。常見的方式包括:

  1. 自訂 Hook(Custom Hook)

  2. 高階元件(Higher-Order Component, HOC)

  3. 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 讓狀態邏輯變得可重用且易於存取。

注意事項

  1. 選擇合適的方式

    • 如果狀態邏輯是局部的(例如計數器、表單輸入),使用自訂 Hook。

    • 如果狀態需要跨多個元件共享(例如主題、使用者資料),使用 Context API 結合自訂 Hook。

  2. 避免過度複雜化

    • 自訂 Hook 應該保持簡單,專注於單一職責。

    • 不要在一個 Hook 中放入過多邏輯,否則會難以維護。

  3. 效能考量

    • 在使用 Context API 時,確保只將必要的資料放入 Context,避免不必要的重新渲染。

    • 使用 useMemo 或 useCallback 來優化複雜的計算或函數。


總結

  1. React 18 的 useEffect 執行兩次

    • 這是 Strict Mode 在開發模式下的行為,模擬元件卸載和重新掛載。

    • 解決方法是確保副作用和清理邏輯正確,接受這是正常行為,或在必要時移除 Strict Mode(不推薦)。

    • 範例程式碼展示了如何正確撰寫 useEffect 和清理邏輯。

  2. Reusable State

    • 使用自訂 Hook(如 useCounter)封裝局部的狀態邏輯,適合簡單、可重用的場景。

    • 使用 Context API 結合自訂 Hook(如 useTheme)實現全域狀態管理,適合跨元件共享的狀態。

    • 範例程式碼展示了如何實作這兩種方式,並確保程式碼簡潔且易於維護。