Skip to main content

useCallback 與 useMemo 的正確使用時機

useCallback 深入解析

什麼是 useCallback?

useCallback 是一個 React Hook,用來記憶一個函數,避免在每次渲染時都重新創建函數的引用。它的主要目的是為了優化效能,特別是在將函數作為 props 傳遞給子元件時,防止子元件因為函數引用改變而導致不必要的重新渲染。

語法

const memoizedCallback = useCallback(() => {
// 你的函數邏輯
}, [dependencies]);
  • 第一個參數:要記憶的函數。

  • 第二個參數:依賴陣列,當依賴項改變時,函數會重新生成。

什麼時候使用 useCallback?

  1. 將函數作為 props 傳遞給子元件,且子元件使用 React.memo

    • 如果子元件被 React.memo 包裝,React 會檢查 props 是否改變來決定是否重新渲染。如果父元件每次渲染都生成一個新的函數引用,子元件會認為 props 改變而重新渲染,即使邏輯沒變。

    • 使用 useCallback 可以確保函數引用不變,減少不必要的渲染。

  2. 避免在依賴陣列中引發不必要的副作用

    • 如果某個函數被其他 Hook(如 useEffect)的依賴陣列引用,則每次渲染生成新函數可能導致副作用被觸發。useCallback 可以穩定函數引用。

範例:使用 useCallback 優化子元件渲染

假設你有一個父元件,傳遞一個函數給子元件,且子元件使用 React.memo 來避免不必要的渲染。

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

// 子元件,使用 React.memo 避免不必要渲染
const ChildComponent = React.memo(({ onClick }) => {
console.log("子元件渲染");
return <button onClick={onClick}>點擊我</button>;
});

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

// 使用 useCallback 記憶函數
const handleClick = useCallback(() => {
console.log("按鈕被點擊");
}, []); // 無依賴,函數永遠不變

return (
<div>
<p>計數: {count}</p>
<button onClick={() => setCount(count + 1)}>增加計數</button>
<ChildComponent onClick={handleClick} />
</div>
);
}

export default ParentComponent;

說明

  • 問題:如果不使用 useCallback,每次 ParentComponent 渲染時,handleClick 都會是一個新的函數引用。即使 ChildComponent 使用了 React.memo,它也會因為 props 改變而重新渲染。

  • 解決:使用 useCallback 記憶 handleClick,確保其引用不變,ChildComponent 只有在必要時才渲染。

  • 依賴陣列:這裡 handleClick 不依賴任何變數,因此依賴陣列為空 []。

注意事項

  1. 不要濫用 useCallback

    • 只有在函數作為 props 傳遞或被其他 Hook 依賴時才需要 useCallback。過度使用會增加程式碼複雜度,且記憶函數本身也有微小的效能開銷。
  2. 確保依賴陣列正確

    • 依賴陣列必須包含函數中使用的所有外部變數,否則可能導致邏輯錯誤。

useMemo 深入解析

什麼是 useMemo?

useMemo 是一個 React Hook,用來記憶一個計算結果,避免在每次渲染時都重新計算昂貴的操作。它的目的是為了優化效能,特別是在計算成本高的情況下。

語法

const memoizedValue = useMemo(() => {
// 昂貴的計算邏輯
return computedValue;
}, [dependencies]);
  • 第一個參數:執行計算並返回結果的函數。

  • 第二個參數:依賴陣列,當依賴項改變時,重新執行計算。

什麼時候使用 useMemo?

  1. 避免昂貴的計算

    • 如果某個計算過程需要大量運算(例如遍歷大陣列、複雜的資料處理),使用 useMemo 可以記憶結果,僅在依賴改變時重新計算。
  2. 穩定傳遞給子元件的 props

    • 類似 useCallback,如果某個計算值作為 props 傳遞給子元件,且子元件使用 React.memo,useMemo 可以確保值的引用不變,避免子元件重新渲染。
  3. 記憶複雜物件或陣列

    • 如果一個物件或陣列在每次渲染時都被重新創建,傳遞給子元件可能導致不必要的渲染。useMemo 可以穩定這些值的引用。

範例:使用 useMemo 優化昂貴計算

假設你有一個元件需要根據輸入資料計算一個排序後的陣列,且這個計算很耗時。

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

function ExpensiveCalculationComponent() {
const [numbers, setNumbers] = useState([5, 2, 8, 1, 9]);
const [count, setCount] = useState(0);

// 使用 useMemo 記憶排序結果
const sortedNumbers = useMemo(() => {
console.log("執行昂貴的排序計算");
return [...numbers].sort((a, b) => a - b);
}, [numbers]); // 僅在 numbers 改變時重新計算

return (
<div>
<p>計數: {count}</p>
<button onClick={() => setCount(count + 1)}>增加計數</button>
<p>原始陣列: {numbers.join(", ")}</p>
<p>排序後陣列: {sortedNumbers.join(", ")}</p>
<button
onClick={() => setNumbers([...numbers, Math.floor(Math.random() * 10)])}
>
新增隨機數字
</button>
</div>
);
}

export default ExpensiveCalculationComponent;

說明

  • 問題:如果不使用 useMemo,每次 count 改變導致元件重新渲染時,排序計算都會重新執行,即使 numbers 沒變。

  • 解決:使用 useMemo 記憶 sortedNumbers,僅在 numbers 改變時重新排序。

  • 依賴陣列:這裡依賴 numbers,因為排序結果取決於它。

注意事項

  1. 不要濫用 useMemo

    • 只有在計算成本高或需要穩定引用時才使用 useMemo。簡單的計算(如加減)通常不需要記憶,因為記憶本身也有開銷。
  2. 確保依賴陣列正確

    • 遺漏依賴可能導致記憶的值過時,造成邏輯錯誤。

useCallback 與 useMemo 的比較與使用時機總結

特性useCallbackuseMemo
用途記憶函數,穩定函數引用記憶計算結果,穩定值或物件引用
返回記憶後的函數記憶後的值
主要場景傳遞函數給子元件、作為其他 Hook 的依賴昂貴計算、穩定傳遞給子元件的物件或陣列
範例場景子元件使用 React.memo 時,傳遞函數 props排序大陣列、生成複雜物件

什麼時候選擇哪個?

  • 用 useCallback:當你需要傳遞一個函數給子元件,或者函數被其他 Hook(如 useEffect)的依賴陣列引用,且希望避免不必要的重新渲染或副作用。

  • 用 useMemo:當你需要執行昂貴的計算,或者需要穩定一個物件/陣列的引用以避免子元件重新渲染。

共同注意事項

  • 依賴陣列必須正確:使用工具如 eslint-plugin-react-hooks 檢查依賴陣列,確保邏輯正確。

  • 避免過度優化:只有在確定有效能問題時才使用這兩個 Hooks,否則會增加程式碼複雜度。

  • React.memo 配合:這兩個 Hooks 通常與 React.memo 一起使用,來優化子元件的渲染。


實務範例:結合 useCallback 和 useMemo

以下是一個結合兩者的完整範例,展示如何在實際專案中優化效能。

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

// 子元件,使用 React.memo
const ListComponent = React.memo(({ items, onItemClick }) => {
console.log("ListComponent 渲染");
return (
<ul>
{items.map((item, index) => (
<li key={index} onClick={() => onItemClick(item)}>
{item}
</li>
))}
</ul>
);
});

function App() {
const [numbers, setNumbers] = useState([5, 2, 8, 1, 9]);
const [count, setCount] = useState(0);

// 使用 useMemo 記憶排序後的陣列
const sortedNumbers = useMemo(() => {
console.log("執行排序計算");
return [...numbers].sort((a, b) => a - b);
}, [numbers]);

// 使用 useCallback 記憶點擊處理函數
const handleItemClick = useCallback((item) => {
console.log(`點擊了: ${item}`);
}, []); // 無依賴,函數永遠不變

return (
<div>
<p>計數: {count}</p>
<button onClick={() => setCount(count + 1)}>增加計數</button>
<button
onClick={() => setNumbers([...numbers, Math.floor(Math.random() * 10)])}
>
新增隨機數字
</button>
<ListComponent items={sortedNumbers} onItemClick={handleItemClick} />
</div>
);
}

export default App;

說明

  • useMemo:記憶 sortedNumbers,確保只有在 numbers 改變時才重新排序。

  • useCallback:記憶 handleItemClick,確保函數引用不變,防止 ListComponent 不必要渲染。

  • React.memo:確保 ListComponent 只有在 items 或 onItemClick 改變時才重新渲染。


結論

  • useCallback 適合用來穩定函數引用,特別是在傳遞給子元件或作為其他 Hook 依賴時。

  • useMemo 適合用來記憶昂貴的計算結果或穩定物件/陣列引用。

  • 兩者都應謹慎使用,避免過度優化導致程式碼複雜。

  • 透過 React.memo 和正確的依賴陣列管理,可以大幅提升 React 應用的效能。