前端框架
-useMemo 教學 - React 的記憶化函數
this.web

React 提供了許多強大的 Hooks,其中 useMemo 屬於是最容易被使用錯誤的 Hook,這次教學就由淺入深介紹 useMemo 的概念、用法與最佳實踐
什麼是 useMemo?
useMemo
是 React 內建的一個 Hook,用於在組件重新渲染期間快取計算結果。
簡單說,useMemo
會回傳一個被記憶化 (memoization) 的值,當我們重新渲染組件時,React 會根據依賴陣列來決定要不要重新計算這個值。
所以說,我們可以透過 useMemo
將耗時的計算結果保存下來,優化效能。
useMemo 的語法
useMemo
的語法很簡潔:
const cachedValue = useMemo(calculateValue, dependencyArray);
calculateValue
是一個不帶參數的函式(通常寫成箭頭函式() => {...}
),用來計算並回傳我們想要緩存的值;dependencyArray
是一個陣列,用於列出計算函式中所使用的所有相依變數(和 useEffect 的依賴陣列用法相同)
React 會在組件初次 render 時執行 calculateValue
,並將結果保存起來。
當組件重新渲染時,React 會將當前的依賴陣列與上一次渲染時的依賴陣列逐一比較。
如果所有依賴陣列的值沒有變化,則 useMemo
直接回傳先前緩存的結果,省略再次執行函數的成本;若依賴陣列有任一項改變,則會重新計算並回傳新值,同時記憶住新的值。
為什麼我們需要 useMemo?
在理解 useMemo
的價值之前,我們先來看看一個常見的錯誤範例。
我們希望組件 mounted
時,去 fetch
使用者資料,同時傳入時間戳給 fetch 函數記錄時間。就像這樣:
export function UserProfileFetcherProblem({ userId }) {
const [userData, setUserData] = useState(null);
const requestOptions = {
includeDetails: true,
source: 'profile-component',
timestamp: Date.now(), // 每次 render 都不同
};
const loadUserData = async (options) => {
const data = await fetchUserDetails(userId, options);
setUserData(data);
};
useEffect(() => {
loadUserData(requestOptions);
}, [requestOptions, loadUserData]); // ⚠️ 每次都不同 → 不斷觸發
return ...
}
結果你會發現這個組件會不斷地重新渲染,導致無窮迴圈或效能問題:
在這段程式中,我們希望 useEffect
僅在 requestOptions
或 loadUserData
改變時才執行,但實際上卻造成每次 render 都觸發 effect。為什麼?
這就要提到一個很核心的觀念 — JS 比較值。
JavaScript 中的原始值與參考值
在 JavaScript 裡,我們比較變數是否「相等」時,會根據其型別有不同行為:
- 原始值(primitive):
number
、string
、boolean
等等,彼此比較時直接比對值是否相同。
const a = 1;
const b = 1;
console.log(a === b); // true
- 參考值(reference):像
object
、array
、function
等,比較時是比「記憶體位置」。
const obj1 = { id: 1 };
const obj2 = { id: 1 };
console.log(obj1 === obj2); // false,不是同一個位置
即使 obj1
和 obj2
的內容完全相同,因為它們是不同的物件,記憶體參考不同,JavaScript 仍會認為它們不相等。
React 是根據「參考是否改變」來決定是否重新觸發副作用
回到 useEffect
的例子中,因為我們在每次 render 時都重新創建了 requestOptions
(一個新的 object)與 loadUserData
(一個新的 function),所以它們的參考值都變了。React 看到依賴變了,就會再次執行 effect,導致:
- 不斷 fetch 資料
- 更新 state(
setUserData
) - 導致 render
- 再次創建新的依賴
- 再次觸發 effect
- 無限迴圈……
解法:使用 useMemo 與 useCallback 穩定參考
要避免這種情況,我們應該在 render 間「穩定」住這些參考值,這就是 useMemo
與 useCallback
的價值
const requestOptions = useMemo(() => ({
includeDetails: true,
source: 'profile-component',
}), []); // 只有初次建立一次
const loadUserData = useCallback(async (options) => {
const data = await fetchUserDetails(userId, options);
setUserData(data);
}, [userId]); // 僅當 userId 改變時重建
現在,這兩個值在依賴不變的情況下會保持相同參考值,useEffect
就不會重複執行。
在這個例子中,我們也可以將 requestOptions
和 loadUserData
放到 useEffect
裡面,就不需要依賴陣列以及記憶化了。
如果你想更深入了解以上內容,可以參考我舉辦的 React 效能優化工作坊,裡面有深入的講解 React 的優化方式以及陷阱。

另一個 useMemo 的應用場景
除了記憶化參考值,useMemo
的另一個場景就是記憶化複雜運算的結果。比如說一個大型清單的篩選或排序,假設你有一個包含數千筆使用者資料的清單,而你需要根據搜尋關鍵字進行過濾:
function UserSearch({ users, keyword }) {
const filteredUsers = users.filter(user =>
user.name.toLowerCase().includes(keyword.toLowerCase())
);
return (
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
這段程式碼在功能上完全正確,但每當父層元件重新 render,即使 users
或 keyword
沒有變,filter
運算還是會重新執行。對於大型資料來說,這可能會導致明顯的卡頓或性能瓶頸。
這時就可以使用 useMemo
來緩存篩選的結果,避免重複運算:
function UserSearch({ users, keyword }) {
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.toLowerCase().includes(keyword.toLowerCase())
);
}, [users, keyword]); // 只有當這兩者變化時才重新運算
return (
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
這樣的優化在使用者輸入過濾條件(例如搜尋關鍵字)時特別有效,因為 useMemo
能保證當 users
與 keyword
都沒變時,就不會重新計算 filter
結果,而是直接使用快取值,從而大幅減少處理開銷、提升互動的流暢度。
使用 useMemo 記憶化複雜運算的前提
但這不代表我們可以把所有運算都用 useMemo 包起來,這會讓程式碼變得難看,也會增加 React 背後的運算成本。
所以我建議,我們必須要先知道這個運算到底複不複雜,再決定是否要使用 useMemo,判斷的方法也很簡單,可以使用 performance 來計算
const start = performance.now();
someExpensiveCalculation();
const end = performance.now();
console.log(`執行時間: ${end - start}ms`);
總結
useMemo
是 React 用來記憶化運算結果的一個 Hook,主要有兩個核心用途:
- 記憶化參考值(Reference):避免因為每次 render 都產生新物件或函式,導致
useEffect
、React.memo
等依賴被誤判為變動。 - 記憶化複雜運算的結果:當某段邏輯涉及大量計算(例如排序、過濾、大型資料處理),可以透過
useMemo
快取結果,僅在必要時才重新運算。
運用得當的 useMemo
可以改善效能,但亂用反而會讓程式碼變難看,所以知道使用時機是非常重要的。