前端框架

-

useMemo 教學 - React 的記憶化函數

this.web

React useMemo 詳細教學與應用封面

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 僅在 requestOptionsloadUserData 改變時才執行,但實際上卻造成每次 render 都觸發 effect。為什麼?

這就要提到一個很核心的觀念 — JS 比較值

JavaScript 中的原始值與參考值

在 JavaScript 裡,我們比較變數是否「相等」時,會根據其型別有不同行為:

  • 原始值(primitive):numberstringboolean 等等,彼此比較時直接比對值是否相同。
const a = 1;
const b = 1;
console.log(a === b); // true
  • 參考值(reference):像 objectarrayfunction 等,比較時是比「記憶體位置」。
const obj1 = { id: 1 };
const obj2 = { id: 1 };
console.log(obj1 === obj2); // false,不是同一個位置

即使 obj1obj2 的內容完全相同,因為它們是不同的物件,記憶體參考不同,JavaScript 仍會認為它們不相等。

React 是根據「參考是否改變」來決定是否重新觸發副作用

回到 useEffect 的例子中,因為我們在每次 render 時都重新創建了 requestOptions(一個新的 object)與 loadUserData(一個新的 function),所以它們的參考值都變了。React 看到依賴變了,就會再次執行 effect,導致:

  • 不斷 fetch 資料
  • 更新 state(setUserData
  • 導致 render
  • 再次創建新的依賴
  • 再次觸發 effect
  • 無限迴圈……

解法:使用 useMemo 與 useCallback 穩定參考

要避免這種情況,我們應該在 render 間「穩定」住這些參考值,這就是 useMemouseCallback 的價值

const requestOptions = useMemo(() => ({
  includeDetails: true,
  source: 'profile-component',
}), []); // 只有初次建立一次

const loadUserData = useCallback(async (options) => {
  const data = await fetchUserDetails(userId, options);
  setUserData(data);
}, [userId]); // 僅當 userId 改變時重建

現在,這兩個值在依賴不變的情況下會保持相同參考值,useEffect 就不會重複執行。

在這個例子中,我們也可以將 requestOptionsloadUserData 放到 useEffect 裡面,就不需要依賴陣列以及記憶化了。

如果你想更深入了解以上內容,可以參考我舉辦的 React 效能優化工作坊,裡面有深入的講解 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,即使 userskeyword 沒有變,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 能保證當 userskeyword 都沒變時,就不會重新計算 filter 結果,而是直接使用快取值,從而大幅減少處理開銷、提升互動的流暢度。

使用 useMemo 記憶化複雜運算的前提

但這不代表我們可以把所有運算都用 useMemo 包起來,這會讓程式碼變得難看,也會增加 React 背後的運算成本。

所以我建議,我們必須要先知道這個運算到底複不複雜,再決定是否要使用 useMemo,判斷的方法也很簡單,可以使用 performance 來計算

const start = performance.now();

someExpensiveCalculation();

const end = performance.now();
console.log(`執行時間: ${end - start}ms`);

總結

useMemo 是 React 用來記憶化運算結果的一個 Hook,主要有兩個核心用途:

  1. 記憶化參考值(Reference):避免因為每次 render 都產生新物件或函式,導致 useEffectReact.memo 等依賴被誤判為變動。
  2. 記憶化複雜運算的結果:當某段邏輯涉及大量計算(例如排序、過濾、大型資料處理),可以透過 useMemo 快取結果,僅在必要時才重新運算。

運用得當的 useMemo 可以改善效能,但亂用反而會讓程式碼變難看,所以知道使用時機是非常重要的。

你可能會感興趣的文章 👇