前端框架

-

useEffect 教學 - React 的副作用管理

this.web

React useEffect 詳細教學與應用 - 封面

什麼是 useEffect?

useEffect 是 React 中非常重要的一個 Hook,它讓函數組件(functional component)可以執行副作用 (Side Effects)

但工作這幾年,我發現有許多工程師並不是真的理解 useEffect 的機制以及含義,所以這篇文章,會從 0 開始帶你徹底了解 useEffect。

useEffect 的語法

useEffect 的語法不難,像這樣:

useEffect(setupFn, dependencies?)

這個 Hook 接收兩個參數:一個是副作用邏輯的函式 setupFn,另一個是選填的依賴陣列 dependencies。

  • setupFn:這是一個函式,當組件完成渲染後會執行這個函數,這裡就是撰寫副作用邏輯的地方。這個函數也可以選擇性地回傳另一個函式,也就是 cleanup function,這個函數會在下一次副作用執行之前或元件卸載時被呼叫用於清除副作用,例如移除事件監聽器或清除計時器。
  • dependencies是一個陣列,用來告訴 React 當哪些值發生變化時,應該重新執行 setupFn。
    • 如果省略這個陣列,副作用會在每次渲染後都執行。
    • 若提供空陣列 [],副作用只會在組件初次掛載時執行一次
    • 若陣列中包含特定的變數(如 [count, userId]),副作用只會在這些變數的值改變時才重新執行。

所以實務上,useEffect 通常會這樣去寫:

import { useEffect } from 'react';

function MyComponent() {
  

  useEffect(() => {  
    // setup logic
    
    return () => {
      // return 一個 function 作為 cleanup function
    };
    
  }, []); // 這裡可以省略陣列、提供空陣列、提供特定變數


  return (...);
}

透過這兩個參數的搭配,我們就可以透過 useEffect 靈活地控制副作用的執行時機,並且提供清理機制。

了解 useEffect 之前要先知道的 Side Effect (副作用)

在文章一開始就提到了副作用,不過對於程式新手來說可能會沒聽過這個詞,但要使用好 useEffect 勢必要先理解副作用是什麼。

什麼是副作用,簡單說:副作用就是這個函數會影響外部環境,這邊我舉一個很簡單的例子:

// 這個函數有副作用 - 它會修改全域變數
let counter = 0;

function increment() {
  counter++; // 副作用:修改了外部的 counter 變數
  return counter;
}

console.log(increment()); // 1
console.log(increment()); // 2
//因為改變了全域變數 counter,導致相同的呼叫會有不同的結果

比如有個全域變數 counter 以及一個函數 increment(),我們在 increment 裡面去修改全域變數 counter,這就讓這個函數有副作用了,因為他影響了外部環境。

可能會讓程式碼變得難以維護,因為你不清楚某些全域變數是不是被改變過了。

當一個函數符合 2 個條件,我們就稱它為 Pure Function(純函數、函式)

  1. 相同輸入永遠得到相同輸出
  2. 沒有副作用

我們跟上個例子比較一下,可以發現 add 這個函數只要傳入的值相同,輸出也永遠會相同,除此之外,他也沒有改變任何的外部環境,所以這就是一個純函數

// 這是一個純函數
function add(a, b) {
  return a + b;
}

// 每次呼叫都會得到相同的結果
console.log(add(2, 3)); // 永遠是 5
console.log(add(2, 3)); // 永遠是 5

透過這個 add 的例子,我們可以比對一般的 React Component,你會發現他也是一個 Pure Function,比如這個 Counter

function Counter(initCount) {
  const [count, setCount] = useState(initCount);
  
  return (
    <div>
      <p>{count}</p>
    </div>
  )
}

相同的輸入(initCount),永遠會是相同的輸出,也沒有任何副作用,這裡的 state 只會影響函數內部,和外部沒有關聯。這也是 React 的核心思維之一 - Pure Function。

useEffect 的核心概念

當了解什麼是 Pure Function,並發現一般的 React Component 就是所謂的 Pure Function 後,我們就能理解 useEffect 的核心概念了。

因為我們的程式一定會有副作用,比如:

  • 發送網路請求取得資料
  • 訂閱外部資料流
  • 操作瀏覽器 API(如文件標題或 localStorage
  • 註冊或移除事件監聽
  • 定時器的設置和清除 … 等等

為了讓我們能處理這些副作用,useEffect 誕生了!useEffect 的核心概念在於管理程式中的副作用,只要是副作用,就能用 useEffect 處理。

useEffect 的執行時機 - 何時開始、何時清理?

要理解 useEffect 的運作方式,首先要知道 React 在每次更新(也就是重新渲染組件)時,會經過兩個主要階段:

  1. Render Phase(渲染階段):React 會重新執行組件函數,產生新的 Virtual DOM。這個階段只是在計算畫面應該長什麼樣子,實際上還沒有改動真實畫面
  2. Commit Phase(提交階段):React 將計算出的變化應用到真實的 DOM,也就是這個時候畫面才真正被更新。

useEffect 的副作用函式(也就是你傳進 useEffect 裡的那個函式)會在 Commit Phase 結束後執行。這代表副作用的執行時間點是畫面更新完成之後,這樣可以確保你操作的是已經呈現在畫面上的元素。

此外,如果你在 useEffect 裡面回傳一個清理函數(Cleanup Function),React 會在執行下一次 effect 前後值行清理函數

簡單來說,每次 useEffect 重新執行前,React 都會先清除前一次的副作用,這可以避免訂閱、計時器或事件監聽累積重複。

這個機制非常重要,它能幫助我們控制副作用的生命週期,確保程式不會重複執行相同操作,也不會留下未清除的事件或資源。對前端新手來說,理解這點是寫出穩定 React 應用的基礎。

延伸閱讀:

React useState 詳細教學

useEffect 的實際範例 - 獲取資料

當我們想要在 React 組件中載入資料時(例如從 API 抓取 JSON),useEffect 是最常用的工具之一。因為資料請求通常是一種副作用,它發生在畫面渲染之外,必須等組件「出現在畫面上」之後才能進行。

在這種情況下,我們可以透過 useEffect 來達成以下流程:

  1. 組件初次掛載後執行資料請求
  2. 請求完成後,透過 setState 將資料存入狀態中
  3. React 會根據資料更新重新渲染畫面
  4. (可選)當元件卸載時取消請求或清理資源
import { useEffect, useState } from 'react';

export function UserList() {
  const [users, setUsers] = useState([]);       // 儲存 API 回傳的使用者資料
  const [loading, setLoading] = useState(true); // 資料是否載入中的狀態
  const [error, setError] = useState(null);     // 錯誤訊息(若發生錯誤)

  useEffect(() => {
    // 定義 async 函式來抓取資料
    const fetchUsers = async () => {
      try {
        const res = await fetch('https://jsonplaceholder.typicode.com/users');
        const data = await res.json();
        
        setUsers(data);         // 儲存資料到 state
      } catch (err) {
        setError(err.message);  // 設定錯誤訊息
      } finally {
        setLoading(false);      // 載入完成
      }
    };

    fetchUsers();

  }, []); // 空陣列代表只在組件初次掛載時執行一次

  // 渲染畫面
  if (loading) return <p>載入中...</p>;
  if (error) return <p>錯誤:{error}</p>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name}{user.email}</li>
      ))}
    </ul>
  );
}

這種資料抓取的模式非常常見,例如在組件載入時從 REST API 取得文章列表、使用者資訊、天氣資料等等。

通常我們會搭配空的依賴陣列 [],表示這個副作用只在組件掛載時執行一次。這樣可以避免每次重新渲染都重複發送請求。

要注意的是,如果請求資料需要依賴某個變數(例如根據 userId 抓取個別使用者的資料),那就必須把該變數加進依賴陣列,確保當 userId 改變時重新取得正確的資料。

useEffect(() => {
  if (!userId) return; // 如果 userId 是 undefined 或 null,則不執行

  const fetchUser = async () => {
    setLoading(true);
    setError(null);
    try {
      // 👇 根據 userId 抓取資料
      const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`); 
      const data = await res.json();
      
      setUser(data);
    } catch (err) {
      setError(err.message);
      setUser(null);
    } finally {
      setLoading(false);
    }
  };

  fetchUser();

}, [userId]); // 依賴 userId,當它變化時重新執行 effect

這種用法是前端開發中最常見的副作用範例之一,幾乎每個需要與伺服器互動的應用都會使用到。所以說,熟悉這個模式,就能幫助你建立基本但重要的資料流處理能力。

爲什麼很多人覺得 useEffect 不好用?

有非常多的工程師會將 useEffect 視為類似 Class Component 的生命週期的替代品。

在 Hook 出來之前,我們有一組明確的生命週期函式,比如 componentDidMountcomponentDidUpdatecomponentWillUnmount。這些函數提供了一個明確的時機點來執行某些操作。

而當 React 推出 useEffect Hook 後,很多工程師自然會把它對應到 class component 的這些生命週期上。很多人對 effect 的想法就是在組件掛載或卸載後後執行某些程式。像這樣:

useEffect(() => {
  console.log('Component did mount or userId changed');
  return () => {
    console.log('Cleanup before unmount or before userId changes again');
  };
}, [userId]);

這種理解方式雖然在某些情況下能幫助我們快速上手,但實際上是限制我們對 useEffect 的認知

使用 useEffect 的注意事項

一、避免無限循環

當你沒有正確地設置依賴陣列時,可能會導致 useEffect 不斷觸發,造成無限循環。通常發生在你在 useEffect 中更新狀態,且將這個狀態作為 useEffect 依賴的時候。

useEffect(() => {
  setCount(count + 1); // 這樣會導致無限循環
}, [count]);

二、我的 effect 執行 2 次

原因是你使用了 React Strict Mode。

React 18 開始,當你使用 <React.StrictMode> 包住應用程式時,React 會在開發模式下針對某些 Hook 進行額外的模擬執行,包含:

  • useEffect
  • useLayoutEffect

這是 React 為了找出不安全副作用的檢查機制。

React 會先執行一次你的組件和其副作用,然後立刻模擬卸載並重新掛載組件,觸發第二次 effect 的執行,這是為了確保這兩次掛載的時的副作用有被清除。

但這個只在開發模式下發生,所以不用擔心,如果你不希望 React 執行 2 次 effect,可以把 <React.StrictMode> 拿掉就好。

三、違反 Hooks 調用規則

和其他的 Hook 一樣,useEffect 不能在循環、條件語句或嵌套函數中使用。

因為 React 依賴 Hook 的調用順序來管理狀態每次渲染時,React 都會按照固定順序執行所有的 Hook,如果 Hook 的順序發生改變,React 將無法正確追蹤狀態,可能導致狀態錯亂或錯誤。

四、將不是副作用的邏輯放到 useEffect

像上個小節提到的,很多工程師會將 useEffect 視為生命週期,於是把無關副作用的邏輯放進來裡面,這是錯誤的做法,某些時候會照成意料之外的 Bug 或效能問題,例如一個打開搜尋框的例子,我們希望在打開搜尋框後,初始其他 state:

useEffect(() => {
  if (isOpen) {
    setSearch('');
    setResults([]);
  }
}, [isOpen]);

但這樣會照成額外的 Re-render,正確做法是將邏輯放到事件中處理:

const handleOpenChange = (open) => {
  setIsOpen(open);
  
  if (open) {
    setSearch('');
    setResults([]);
  }
};

useEffect 的總結

useEffect 是 React 函數組件中處理副作用(Side Effects)的核心工具,幾乎每一個與外部互動的應用場景(如資料請求、事件監聽、DOM 操作)都會用到它。

透過 useEffect,我們就能在畫面渲染完成後有條理地管理副作用,並根據依賴條件自動更新,同時也能清理資源。

你可能會感興趣的文章 👇