React useCallback 如何解決 useEffect 無限循環問題

Posted by Young on 2025-07-21
Estimated Reading Time 3 Minutes
Words 813 In Total

問題

直接先看問題,原本照下方程式碼這樣子去調用 fetchCustomerData() 的話,會出現 React Hook useEffect has a missing dependency: 'fetchCustomerData'. Either include it or remove the dependency array. 的警告訊息

useffect-error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
useEffect(() => {
fetchCustomerData();
}, [customerId]);

const fetchCustomerData = async () => {
try {
setLoading(true);
const [customerResponse, ordersResponse, transactionsResponse] = await Promise.all([
api.get<Customer>(`/customers/${customerId}/`),
api.get<Order[]>(`/customers/${customerId}/orders/`),
api.get<Transaction[]>(`/customers/${customerId}/transactions/`)
]);

setCustomer(customerResponse.data);
setOrders(ordersResponse.data);
setTransactions(transactionsResponse.data);
} catch (err: unknown) {
setError('無法取得客戶資料');
console.error('Error fetching customer:', err);
} finally {
setLoading(false);
}
};
...

如果不去理解的話,就會以為只要單純把 fetchCustomerData 放進 useEffect 的依賴陣列,並且將 fetchCustomerData 函數放在 useEffect 之前定義就可以解決問題

1
2
3
4
5
6
7
8
const fetchCustomerData = async () => {
console.log("fetchCustomerData called");
...
};

useEffect(() => {
fetchCustomerData();
}, [customerId, fetchCustomerData]);

當然很快的你就會發現 IDE 就會直接警告你會觸發無限 re-render 了,因為每次 fetchCustomerData 被調用時都會重新創建一個新的函式,又會觸發 useEffect 的依賴檢查,導致 useEffect 無限循環

unlimit-rerender

原因:

  1. 第一次渲染:
    • 創建 fetchCustomerData 函數 (函數引用 = function#1)
    • useEffect 執行,依賴是 [customerId: 123, function#1]
  2. useEffect 執行後觸發重新渲染:
    • 重新創建 fetchCustomerData 函數 (函數引用 = function#2)
    • React 比較依賴:[customerId: 123, function#2] ≠ [customerId: 123, function#1] - 因為函數引用不同,觸發 useEffect
  3. 無限循環開始:
    • 渲染 → 新函數 → useEffect 觸發 → 重新渲染 → 新函數 → useEffect 觸發 → …

解決方法

使用 useCallback 包裝 fetchCustomerData 函數,並且指定 customerId 為依賴,這樣可以確保只有當 customerId 改變時才會重新創建函數,從而避免無限循環的問題。

1
2
3
4
5
6
7
const fetchCustomerData = useCallback(async () => {
...
}, [customerId]); // 只有當 customerId 改變時才重新建立函數

useEffect(() => {
fetchCustomerData();
}, [customerId, fetchCustomerData]);

觀念補充

這也是在技術面試中常會遇到的問題,比如會問 React 是如何判斷何時該重新渲染組件的,或是應該如何避免不必要的渲染等問題

這是就要好好複習基本觀念也就是 React 並不會深度比較物件或陣列的內容,而是只檢查它們的「記憶體位置」(也就是參考位址)有沒有改變。如果兩個物件內容一樣,但記憶體位置不同,React 會認為它們不一樣;反之,如果直接修改原本的物件內容而沒創造新物件,React 則會以為資料沒變,導致畫面不更新。

這也是為什麼在 React 中強調 「不可變資料(immutable data)」的原則 —— 只要資料變更,就應該建立一個新物件或新陣列,這樣 React 才能正確判斷有變化並重新渲染畫面。

1
2
3
4
5
6
// 每次渲染
const func1 = () => {};
const func2 = () => {};

console.log(func1 === func2); // false! 即使函數內容相同
console.log(123 === 123); // true (customerId

同樣概念也適用 useEffect 或 useCallback 等 Hook 的 依賴陣列中。如果你在每次渲染時都產生一個新的函式或物件,React 就會認為依賴變了,進而導致 effect 重新執行或元件重新渲染。因此正確管理依賴與避免不必要的重新建立,是提升效能的關鍵。


若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~


留言版