useCallback 與 useMemo 的正確使用時機
useCallback 深入解析
什麼是 useCallback?
useCallback 是一個 React Hook,用來記憶一個函數,避免在每次渲染時都重新創建函數的引用。它的主要目的是為了優化效能,特別是在將函數作為 props 傳遞給子元件時,防止子元件因為函數引用改變而導致不必要的重新渲染。
語法
const memoizedCallback = useCallback(() => {
// 你的函數邏輯
}, [dependencies]);
-
第一個參數:要記憶的函數。
-
第二個參數:依賴陣列,當依賴項改變時,函數會重新生成。
什麼時候使用 useCallback?
-
將函數作為 props 傳遞給子元件,且子元件使用 React.memo:
-
如果子元件被 React.memo 包裝,React 會檢查 props 是否改變來決定是否重新渲染。如果父元件每次渲染都生成一個新的函數引用,子元件會認為 props 改變而重新渲染,即使邏輯沒變。
-
使用 useCallback 可以確保函數引用不變,減少不必要的渲染。
-
-
避免在依賴陣列中引發不必要的副作用:
- 如果某個函數被其他 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 不依賴任何變數,因此依賴陣列為空 []。
注意事項
-
不要濫用 useCallback:
- 只有在函數作為 props 傳遞或被其他 Hook 依賴時才需要 useCallback。過度使用會增加程式碼複雜度,且記憶函數本身也有微小的效能開銷。
-
確保依賴陣列正確:
- 依賴陣列必須包含函數中使用的所有外部變數,否則可能導致邏輯錯誤。
useMemo 深入解析
什麼是 useMemo?
useMemo 是一個 React Hook,用來記憶一個計算結果,避免在每次渲染時都重新計算昂貴的操作。它的目的是為了優化效能,特別是在計算成本高的情況下。
語法
const memoizedValue = useMemo(() => {
// 昂貴的計算邏輯
return computedValue;
}, [dependencies]);
-
第一個參數:執行計算並返回結果的函數。
-
第二個參數:依賴陣列,當依賴項改變時,重新執行計算。
什麼時候使用 useMemo?
-
避免昂貴的計算:
- 如果某個計算過程需要大量運算(例如遍歷大陣列、複雜的資料處理),使用 useMemo 可以記憶結果,僅在依賴改變時重新計算。
-
穩定傳遞給子元件的 props:
- 類似 useCallback,如果某個計算值作為 props 傳遞給子元件,且子元件使用
React.memo
,useMemo 可以確保值的引用不變,避免子元件重新渲染。
- 類似 useCallback,如果某個計算值作為 props 傳遞給子元件,且子元件使用
-
記憶複雜物件或陣列:
- 如果一個物件或陣列在每次渲染時都被重新創建,傳遞給子元件可能導致不必要的渲染。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,因為排序結果取決於它。
注意事項
-
不要濫用 useMemo:
- 只有在計算成本高或需要穩定引用時才使用 useMemo。簡單的計算(如加減)通常不需要記憶,因為記憶本身也有開銷。
-
確保依賴陣列正確:
- 遺漏依賴可能導致記憶的值過時,造成邏輯錯誤。
useCallback 與 useMemo 的比較與使用時機總結
特性 | useCallback | useMemo |
---|---|---|
用途 | 記憶函數,穩定函數引用 | 記憶計算結果,穩定值或物件引用 |
返回 | 記憶後的函數 | 記憶後的值 |
主要場景 | 傳遞函數給子元件、作為其他 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 應用的效能。