Skip to main content

useEffect 其實不是 function component 的生命週期 API

useEffect 是宣告式的同步化,而非生命週期 API

什麼是「宣告式的同步化」?

在 React 中,useEffect 的設計並不是為了直接模擬類別元件(class component)的生命週期方法(例如 componentDidMount、componentDidUpdate、componentWillUnmount),而是用來讓你的副作用(side effect)與元件的狀態或屬性(props)保持同步。它的核心理念是「宣告式」的:你告訴 React 你想要在什麼條件下執行什麼副作用,React 會負責在適當的時機執行它。

具體來說:

  • 副作用(side effect):像是資料請求(fetch data)、訂閱事件、操作 DOM 等,這些操作不在 React 的控制範圍內。

  • 宣告式:你只需要定義「什麼時候(依賴項改變時)要做什麼(執行某個副作用)」,React 會自動根據元件的渲染時機來調度這些副作用。

  • 同步化:useEffect 確保你的副作用與元件當前的狀態或屬性保持一致。例如,當某個狀態改變時,你可能需要重新發送 API 請求來獲取新資料。

這與類別元件的生命週期方法不同,後者是命令式的(imperative),你需要明確指定在「某個生命週期階段」做什麼(例如「掛載時」或「更新時」)。useEffect 則更專注於「資料與副作用的同步」,而不是特定的生命週期階段。

程式碼範例:展示宣告式的同步化

假設你正在開發一個顯示使用者資料的元件,當 userId 改變時,需要重新從 API 獲取資料。

import React, { useState, useEffect } from "react";

function UserProfile({ userId }) {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
// 定義副作用:當 userId 改變時,發送 API 請求
async function fetchUserData() {
setLoading(true);
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
setUserData(data);
} catch (error) {
console.error("錯誤:", error);
} finally {
setLoading(false);
}
}

fetchUserData();
}, [userId]); // 當 userId 改變時,重新執行副作用

if (loading) {
return <div>載入中...</div>;
}

return (
<div>
<h1>{userData?.name}</h1>
<p>{userData?.email}</p>
</div>
);
}

export default UserProfile;

範例解析

  1. 宣告式思維

    • 你在 useEffect 中宣告:「當 userId 改變時,執行 fetchUserData 來更新資料。」

    • 你不需要明確指定「這是在掛載時執行」或「這是在更新時執行」,React 會根據 userId 的變化自動決定什麼時候執行。

  2. 同步化

    • useEffect 確保 userData 與 userId 保持同步。只要 userId 改變,useEffect 就會重新執行,觸發 API 請求,更新 userData。
  3. 非生命週期

    • 這裡並沒有明確的「掛載」或「更新」階段,useEffect 的執行完全由依賴項 [userId] 驅動。這與類別元件的 componentDidMount 或 componentDidUpdate 的命令式思維不同。

與類別元件的對比

在類別元件中,你可能會這樣寫:

import React, { Component } from "react";

class UserProfile extends Component {
state = {
userData: null,
loading: true,
};

componentDidMount() {
this.fetchUserData(this.props.userId);
}

componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUserData(this.props.userId);
}
}

async fetchUserData(userId) {
this.setState({ loading: true });
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
this.setState({ userData: data });
} catch (error) {
console.error("錯誤:", error);
} finally {
this.setState({ loading: false });
}
}

render() {
if (this.state.loading) {
return <div>載入中...</div>;
}

return (
<div>
<h1>{this.state.userData?.name}</h1>
<p>{this.state.userData?.email}</p>
</div>
);
}
}

export default UserProfile;

對比分析

  • 類別元件需要你明確區分「掛載」(componentDidMount)和「更新」(componentDidUpdate),並手動比較 prevProps.userIdthis.props.userId 是否不同。

  • useEffect 則簡化了這個過程:你只需要指定依賴項 [userId],React 會自動在適當的時機執行副作用。


Dependencies 是一種效能優化,而非執行時機的控制

什麼是 Dependencies 的角色?

useEffect 的第二個參數(依賴項陣列,dependency array)是用來控制副作用的執行頻率,以避免不必要的重複執行,從而達到效能優化的目的。它並不是用來「精確控制執行時機」(例如「只在掛載時執行」),而是告訴 React:「只有當這些依賴項改變時,才需要重新執行副作用。」

為什麼說是效能優化?

  • 如果沒有依賴項陣列(或傳入空陣列 []),useEffect 會在每次渲染後都執行,這可能導致不必要的副作用執行(例如重複的 API 請求)。

  • 通過正確設定依賴項陣列,你可以限制副作用只在「必要的時候」執行,減少無謂的計算或網路請求。

  • 依賴項陣列的本質是 React 用來比較前後渲染的依賴值是否改變,進而決定是否執行副作用。

程式碼範例:展示依賴項的效能優化

假設你有一個計數器元件,當計數改變時,你想記錄到日誌(log),但只有在特定條件下才執行。

import React, { useState, useEffect } from "react";

function Counter() {
const [count, setCount] = useState(0);
const [userId, setUserId] = useState(1);

// 副作用:記錄計數到日誌
useEffect(() => {
console.log(`計數改變,現在是 ${count}`);
// 假設這是一個發送到後端的日誌記錄
fetch("https://api.example.com/log", {
method: "POST",
body: JSON.stringify({ count, userId }),
});
}, [count, userId]); // 只有 count 或 userId 改變時才執行

return (
<div>
<p>目前計數: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<button onClick={() => setUserId(userId + 1)}>切換使用者</button>
</div>
);
}

export default Counter;

範例解析

  1. 依賴項的作用

    • useEffect 的依賴項是 [count, userId],這意味著只有當 count 或 userId 改變時,副作用(記錄日誌)才會執行。

    • 如果你點擊「增加」按鈕,count 改變,副作用執行;如果你點擊「切換使用者」,userId 改變,副作用也執行。

    • 如果元件因為其他原因重新渲染(例如父元件傳入不相關的 props),但 count 和 userId 沒變,副作用就不會執行,這就是效能優化

  2. 錯誤用法:沒有依賴項: 如果你移除依賴項陣列(useEffect(() => {...})),副作用會在每次渲染後都執行,這可能導致不必要的日誌記錄或 API 請求,浪費資源。

  3. 錯誤用法:空陣列: 如果你傳入空陣列(useEffect(() => {...}, \[\])),副作用只會在元件掛載時執行一次,但如果 count 或 userId 改變,副作用不會重新執行,這可能導致資料不同步。

常見誤解:依賴項控制執行時機

有些人誤以為依賴項陣列是用來「控制副作用在什麼時候執行」(例如模擬 componentDidMount)。這是不正確的。依賴項的真正目的是限制不必要的副作用執行,而不是強制指定某個生命週期階段。以下是常見的錯誤用法:

// 錯誤:試圖模擬 componentDidMount
useEffect(() => {
console.log("這只在掛載時執行");
}, []); // 空陣列只在掛載時執行,但這不是 useEffect 的主要目的

// 正確:應該專注於同步化
useEffect(() => {
console.log("當 userId 改變時,執行某些副作用");
}, [userId]); // 專注於 userId 的同步

總結與實務建議

  1. useEffect 是宣告式的同步化

    • 它的核心是讓副作用與狀態/屬性保持同步,而不是模擬生命週期。

    • 透過宣告依賴項,你告訴 React 什麼時候需要重新執行副作用,無需手動管理生命週期。

  2. 依賴項是效能優化工具

    • 依賴項陣列的目的是避免不必要的副作用執行,而不是精確控制執行時機。

    • 正確設定依賴項可以顯著提升應用程式的效能,避免浪費資源。

  3. 實務建議

    • 總是明確列出所有在 useEffect 中使用的狀態或屬性作為依賴項,避免資料不同步。

    • 使用 ESLint 的 exhaustive-deps 規則,確保依賴項陣列正確無誤。

    • 如果需要清理副作用(例如取消訂閱或清除計時器),在 useEffect 中回傳一個清理函數:

    useEffect(() => {
    const timer = setInterval(() => {
    console.log("計時器執行中");
    }, 1000);

    return () => clearInterval(timer); // 清理副作用
    }, []);