Skip to main content

維護資料流的連動:不要欺騙 hooks 的 dependencies

欺騙 dependencies 會造成什麼問題

在 React 的 useEffect 或其他需要 dependencies 的 hooks(如 useMemo, useCallback)中,dependencies 陣列是用來告訴 React 哪些變數或狀態會影響該 effect 的執行。如果「欺騙」dependencies(例如故意忽略某些依賴項,或錯誤設定),會導致以下問題:

  1. 狀態不同步:當你忽略某個依賴項,effect 可能無法正確響應狀態或 props 的變化,導致 UI 顯示舊資料或不一致的行為。

  2. 無窮迴圈:若錯誤地設定 dependencies,可能導致 effect 無限次執行,造成應用程式卡頓或崩潰。

  3. 邏輯錯誤:例如,某個 effect 依賴外部變數,但因為未列入 dependencies,effect 不會在變數改變時重新執行,導致邏輯不符合預期。

  4. 難以維護:欺騙 dependencies 會讓程式碼行為變得不直觀,後續維護或 debug 時難以追蹤問題。

範例:忽略依賴項的問題

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

function ExampleComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState("Alice");

useEffect(() => {
console.log(`Count is ${count}, Name is ${name}`);
}, [count]); // 錯誤:忽略了 name 的依賴

return (
<div>
<p>Count: {count}</p>
<p>Name: {name}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setName("Bob")}>Change Name</button>
</div>
);
}

問題

  • useEffect 依賴 count 和 name,但只列了 count。

  • 當 name 改變時,useEffect 不會重新執行,導致無法正確記錄最新的 name。

解決方式: 將所有在 useEffect 中使用的變數都加入 dependencies 陣列:

useEffect(() => {
console.log(`Count is ${count}, Name is ${name}`);
}, [count, name]); // 正確:包含所有依賴

讓 effect 函式對於依賴的資料自給自足

為了避免依賴過多或不必要的重新渲染,應該讓 useEffect 的邏輯盡可能「自給自足」,即減少對外部變數的依賴。這可以通過以下方式實現:

  1. 將邏輯移到 effect 內部:如果某個計算可以直接在 useEffect 中完成,就不要依賴外部變數。

  2. 使用 useMemo 或 useCallback:對於複雜的計算或函數,確保它們在 dependencies 中是穩定的引用。

  3. 提取獨立函數:將 effect 內的邏輯封裝成獨立函數,減少外部變數的使用。

範例:自給自足的 effect

問題範例:依賴外部變數

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

function ExampleComponent() {
const [userId, setUserId] = useState(1);
const [userData, setUserData] = useState(null);

// 外部定義的 API URL,會被多個地方使用
const apiBaseUrl = "https://api.example.com";
const userEndpoint = `/users/${userId}`;

useEffect(() => {
// 依賴外部變數,增加了不必要的依賴
fetch(`${apiBaseUrl}${userEndpoint}`)
.then((response) => response.json())
.then((data) => setUserData(data));
}, [userId, apiBaseUrl, userEndpoint]); // 依賴過多外部變數

return (
<div>
<p>User ID: {userId}</p>
<p>User Data: {userData?.name || "Loading..."}</p>
<button onClick={() => setUserId(userId + 1)}>Next User</button>
</div>
);
}

修正後:自給自足的 effect

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

function ExampleComponent() {
const [userId, setUserId] = useState(1);
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(false);

useEffect(() => {
// 將所有需要的邏輯和資料都放在 effect 內部
const fetchUserData = async () => {
setLoading(true);
try {
// 在 effect 內部定義 API 相關的邏輯
const apiUrl = `https://api.example.com/users/${userId}`;
const response = await fetch(apiUrl);
const data = await response.json();
setUserData(data);
} catch (error) {
console.error("Failed to fetch user data:", error);
setUserData(null);
} finally {
setLoading(false);
}
};

fetchUserData();
}, [userId]); // 只依賴真正需要的狀態

return (
<div>
<p>User ID: {userId}</p>
<p>User Data: {loading ? "Loading..." : userData?.name || "No data"}</p>
<button onClick={() => setUserId(userId + 1)}>Next User</button>
</div>
);
}

說明

  • 副作用處理:useEffect 現在處理真正的副作用(API 請求、狀態更新)。
  • 自給自足:所有 API 邏輯都封裝在 effect 內部,不依賴外部變數。
  • 簡化依賴:dependencies 陣列只包含真正影響 effect 行為的 userId
  • 錯誤處理:包含適當的錯誤處理和載入狀態管理。

函式型別的依賴

當 useEffect 依賴一個函數(例如來自 props 或內部定義的函數),因為函數在每次渲染時可能會產生新的引用,會導致 useEffect 頻繁執行。解決方式是使用 useCallback 來穩定函數引用。

範例:不穩定的函數依賴

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

function ExampleComponent({ fetchData }) {
const [count, setCount] = useState(0);

// 每次渲染都會產生新的 fetchData 函數引用
useEffect(() => {
fetchData(count);
}, [fetchData, count]); // fetchData 是動態的,導致 effect 頻繁執行

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

解決方式:使用 useCallback

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

function ExampleComponent() {
const [count, setCount] = useState(0);

// 使用 useCallback 穩定函數引用
const fetchData = useCallback((value) => {
console.log(`Fetching data with count: ${value}`);
}, []); // 空陣列表示 fetchData 不會改變

useEffect(() => {
fetchData(count);
}, [fetchData, count]); // fetchData 是穩定的,不會頻繁觸發

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

說明

  • useCallback 確保 fetchData 的引用在元件生命周期中保持穩定,除非其依賴改變。

  • 這樣可以避免 useEffect 因函數引用變化而頻繁執行。


以 linter 來輔助填寫 dependencies

React 提供 ESLint 插件(如 eslint-plugin-react-hooks)來檢查 useEffect、useMemo 和 useCallback 的 dependencies 是否正確。這個工具可以幫助你避免遺漏或錯誤設定依賴項。

安裝與設定 ESLint 插件

  1. 安裝必要套件:

    npm install eslint-plugin-react-hooks --save-dev
  2. 在 ESLint 設定檔(.eslintrc.json)中加入插件:

    {
    "plugins": ["react-hooks"],
    "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
    }
    }

使用範例

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

function UserProfile() {
const [userId, setUserId] = useState(1);
const [username, setUsername] = useState("john_doe");
const [profile, setProfile] = useState(null);

useEffect(() => {
// 副作用:API 請求和本地存儲
const fetchProfile = async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setProfile(data);

// 同時更新本地存儲
localStorage.setItem(`user_${username}_profile`, JSON.stringify(data));
};

fetchProfile();
}, [userId]); // ESLint 會警告缺少 username 依賴
}

ESLint 警告

React Hook useEffect has a missing dependency: 'username'. Either include it or remove the dependency array.

修正方式: 根據 ESLint 建議,將 username 加入 dependencies:

useEffect(() => {
const fetchProfile = async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setProfile(data);

// 同時更新本地存儲
localStorage.setItem(`user_${username}_profile`, JSON.stringify(data));
};

fetchProfile();
}, [userId, username]); // 修正後:包含所有依賴

建議

  • 啟用 eslint-plugin-react-hooks,並在程式碼編輯器中即時檢查(例如 VS Code)。

  • 遵循 ESLint 提示,確保 dependencies 陣列完整。


Effect dependencies 常見的錯誤用法

以下是使用 useEffect dependencies 時常見的錯誤,以及如何修正:

錯誤 1:忽略依賴項

useEffect(() => {
console.log(count);
}, []); // 錯誤:count 被使用但未列入

修正

useEffect(() => {
console.log(count);
}, [count]); // 加入 count

錯誤 2:依賴不穩定的物件或函數

const fetchData = () => console.log("Fetching...");
useEffect(() => {
fetchData();
}, [fetchData]); // 錯誤:fetchData 每次渲染都不同

修正

const fetchData = useCallback(() => console.log("Fetching..."), []);
useEffect(() => {
fetchData();
}, [fetchData]); // fetchData 穩定

錯誤 3:過度依賴

const [count, setCount] = useState(0);
const [name, setName] = useState("Alice");

useEffect(() => {
console.log(count); // 只使用 count
}, [count, name]); // 錯誤:name 不必要

修正

useEffect(() => {
console.log(count);
}, [count]); // 只包含必要的 count

錯誤 4:無限迴圈

useEffect(() => {
setCount(count + 1); // 每次渲染都更新 count
}, [count]); // 導致無限迴圈

修正: 避免在 useEffect 中直接更新依賴的狀態,或者使用條件邏輯:

useEffect(() => {
if (count < 5) {
setCount(count + 1); // 限制更新次數
}
}, [count]);

總結與建議

  1. 遵循 ESLint 規則:使用 eslint-plugin-react-hooks 確保 dependencies 正確。

  2. 保持自給自足:將計算邏輯放入 useEffect 內,減少外部依賴。

  3. 穩定函數引用:使用 useCallback 穩定函數,減少不必要的重新渲染。

  4. 檢查邏輯:確保 useEffect 的行為符合預期,避免無限迴圈或狀態不同步。