useMemo 是一個 React Hook,可讓您在重新渲染之間快取計算結果。

const cachedValue = useMemo(calculateValue, dependencies)

參考

useMemo(calculateValue, dependencies)

在元件的頂層呼叫 useMemo 以快取重新渲染之間的計算結果

import { useMemo } from 'react';

function TodoList({ todos, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
// ...
}

請參閱以下更多範例。

參數

  • calculateValue:計算您想要快取的值的函式。它應該是純粹的,不應該接受任何參數,並且應該返回任何類型的值。 React 將在初始渲染期間呼叫您的函式。在下一次渲染時,如果自上次渲染以來 dependencies 沒有更改,React 將再次返回相同的值。否則,它將呼叫 calculateValue,返回其結果,並將其儲存以便稍後重複使用。

  • dependenciescalculateValue程式碼內參考的所有反應值的清單。反應值包括 props、state 以及直接在元件主體內宣告的所有變數和函式。如果您的程式碼檢查器已 針對 React 進行設定,它將驗證每個反應值是否已正確指定為依賴項。依賴項清單必須具有固定數量的項目,並且像 [dep1, dep2, dep3] 一樣內嵌編寫。 React 將使用 Object.is 比較將每個依賴項与其先前的值進行比較。

回傳值

在初始渲染時,useMemo 回傳呼叫 calculateValue 且不帶參數的結果。

在後續渲染期間,它將回傳上次渲染中已儲存的值(如果依賴項未更改),或者再次呼叫 calculateValue 並回傳 calculateValue 回傳的結果。

注意事項
  • useMemo 是一個 Hook,所以你只能在組件的最頂層或你自定義的 Hooks 中調用它。你不能在迴圈或條件語句中調用它。如果你需要這樣做,請提取一個新的組件並將狀態移入其中。
  • 在嚴格模式下,React 會調用你的計算函式兩次,以便幫助你發現意外的非純粹性。 這僅在開發環境中發生,不會影響生產環境。如果你的計算函式是純粹的(它 seharusnya如此),這不應該影響你的邏輯。其中一次調用的結果將被忽略。
  • React 除非有特定原因,否則不會丟棄已快取的值。 例如,在開發環境中,當你編輯組件文件時,React 會丟棄快取。在開發和生產環境中,如果你的組件在初始掛載期間暫停,React 都會丟棄快取。未來,React 可能會添加更多利用丟棄快取的功能 — 例如,如果 React 將來添加對虛擬化列表的內建支援,那麼將滾出虛擬化表格視埠的項目的快取丟棄是有意義的。如果你僅將 useMemo 作為效能優化手段,這應該沒問題。否則,狀態變數ref 可能更合適。
  • 注意事項

    像這樣快取返回值也稱為記憶化 (memoization)這就是為什麼這個 Hook 被稱為 useMemo 的原因。


    用法

    跳過耗時的重新計算

    要在重新渲染之間快取計算結果,請將其包裝在組件頂層的 useMemo 調用中

    import { useMemo } from 'react';

    function TodoList({ todos, tab, theme }) {
    const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
    // ...
    }

    你需要傳遞兩個參數給 useMemo

    1. 一個不帶參數的計算函式,例如 () =>,並返回你想要計算的結果。
    2. 一個依賴項列表,包含組件中在計算內使用的每個值。

    在初始渲染時,你從 useMemo 獲得的將是調用你的計算的結果。

    在每次後續渲染時,React 會將依賴項與你在上次渲染期間傳遞的依賴項進行比較。如果沒有依賴項發生更改(與Object.is比較),useMemo 將返回你之前已經計算出的值。否則,React 將重新運行你的計算並返回新值。

    換句話說,useMemo 會在重新渲染之間快取計算結果,直到其依賴項發生更改為止。

    讓我們通過一個例子來看看它什麼時候有用。

    默認情況下,React 會在每次重新渲染時重新運行組件的整個主體。例如,如果這個 TodoList 更新其狀態或從其父組件接收新的 props,filterTodos 函式將重新運行

    function TodoList({ todos, tab, theme }) {
    const visibleTodos = filterTodos(todos, tab);
    // ...
    }

    通常,這不是問題,因為大多數計算都非常快。但是,如果你正在過濾或轉換大型陣列,或者正在進行一些耗時的計算,你可能希望在資料未更改的情況下跳過再次執行它。如果 todostab 都與上次渲染時相同,則像前面一樣將計算包裝在 useMemo 中,可以讓你重複使用之前已經計算出的 visibleTodos

    這種類型的快取稱為 *記憶化 (memoization)。*

    注意事項

    你應該僅將 useMemo 作為效能優化手段。 如果你的程式碼沒有它就無法正常工作,請先找到根本問題並修復它。然後你可以添加 useMemo 來提高效能。

    深入探討

    如何判斷計算是否耗時?

    一般來說,除非你正在創建或迴圈數千個物件,否則它可能不會很耗時。如果你想更加確定,可以添加一個 console.log 來測量一段程式碼所花費的時間

    console.time('filter array');
    const visibleTodos = filterTodos(todos, tab);
    console.timeEnd('filter array');

    執行你要測量的互動(例如,在輸入框中輸入)。然後你將在控制台中看到類似 filter array: 0.15ms 的日誌。如果記錄的總時間加起來相當可觀(例如,1ms 或更多),則記憶化該計算可能是有意義的。作為實驗,你可以將計算包裝在 useMemo 中,以驗證該互動的記錄總時間是否減少

    console.time('filter array');
    const visibleTodos = useMemo(() => {
    return filterTodos(todos, tab); // Skipped if todos and tab haven't changed
    }, [todos, tab]);
    console.timeEnd('filter array');

    useMemo 不會使*第一次*渲染更快。它只幫助你跳過更新時不必要的工作。

    請記住,你的機器可能比你的用戶的機器快,因此最好通過人為減速來測試效能。例如,Chrome 提供了CPU 節流選項來實現此目的。

    另請注意,在開發環境中測量效能不會給你最準確的結果。(例如,當嚴格模式開啟時,你將看到每個組件渲染兩次而不是一次。)要獲得最準確的時間,請將你的應用程式構建用於生產環境,並在與你的用戶相同的設備上進行測試。

    深入探討

    是否應該在任何地方都使用 useMemo?

    如果你的應用程式像這個網站一樣,大多數的互動都是粗略的(例如替換整個頁面或整個區塊),通常不需要記憶體優化。另一方面,如果你的應用程式更像是一個繪圖編輯器,大多數的互動都是細微的(例如移動形狀),那麼你可能會發現記憶體優化非常有幫助。

    僅在少數情況下使用 useMemo 進行優化才有價值

    • 你在 useMemo 中執行的計算明顯緩慢,而且它的依賴項很少更改。
    • 你將它作為 prop 傳遞給一個用 memo 包裝的組件。如果你希望在值未更改時跳過重新渲染,記憶體優化可讓你的組件僅在依賴項不同時才重新渲染。
    • 你傳遞的值稍後會被用作某些 Hook 的依賴項。例如,另一個 useMemo 計算值可能依賴於它。或者你可能在 useEffect 中依賴於這個值。

    在其他情況下,將計算包裝在 useMemo 中沒有任何好處。這樣做也沒有顯著的壞處,因此有些團隊選擇不去考慮個別情況,而是盡可能地進行記憶體優化。這種方法的缺點是程式碼變得難以閱讀。此外,並非所有記憶體優化都有效:一個「總是新的」值就足以破壞整個組件的記憶體優化。

    在實務中,你可以透過遵循一些原則來避免許多不必要的記憶體優化

    1. 當一個組件在視覺上包裝其他組件時,讓它接受 JSX 作為子元素。這樣,當包裝器組件更新其自身的狀態時,React 就知道它的子元素不需要重新渲染。
    2. 盡量使用局部狀態,並且不要將狀態提升到不必要的層級。例如,不要將表單等暫態狀態,或某個項目是否被 hover 的狀態,保存在樹的頂層或全域狀態庫中。
    3. 保持你的渲染邏輯純粹。如果重新渲染一個組件會導致問題或產生一些明顯的視覺錯誤,那就是你的組件中有 bug!修復 bug,而不是添加記憶體優化。
    4. 避免不必要的 Effect 更新狀態。React 應用程式中的大多數效能問題都是由 Effect 引起的更新鏈所導致的,這些更新會導致你的組件不斷地重新渲染。
    5. 嘗試從你的 Effect 中移除不必要的依賴項。例如,與其進行記憶體優化,通常更簡單的做法是將某些物件或函式移到 Effect 內部或組件外部。

    如果特定的互動仍然感覺延遲,請使用 React 開發者工具分析器來查看哪些組件最能從記憶體優化中受益,並在需要的地方添加記憶體優化。 這些原則使你的組件更容易除錯和理解,因此在任何情況下都應該遵循它們。從長遠來看,我們正在研究自動執行細粒度記憶體優化,以一勞永逸地解決這個問題。

    useMemo 和直接計算值的區別

    範例 1說明 2:
    使用 useMemo 跳過重新計算

    在此範例中,filterTodos 的實作被**人為地減慢**,以便你可以看到在渲染期間呼叫某些 JavaScript 函式時,如果它真的很慢會發生什麼事。嘗試切換分頁和切換主題。

    切換分頁感覺很慢,因為它會強制重新執行被減慢的 filterTodos。這是預期的,因為 tab 已經改變,因此整個計算*需要*重新執行。(如果你好奇為什麼它會執行兩次,這裡有說明。)

    切換主題。**由於使用了 useMemo,即使人為減慢速度,它仍然很快!** 緩慢的 filterTodos 呼叫被跳過了,因為 todostab(你將它們作為依賴項傳遞給 useMemo)自上次渲染以來都沒有改變。

    import { useMemo } from 'react';
    import { filterTodos } from './utils.js'
    
    export default function TodoList({ todos, theme, tab }) {
      const visibleTodos = useMemo(
        () => filterTodos(todos, tab),
        [todos, tab]
      );
      return (
        <div className={theme}>
          <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
          <ul>
            {visibleTodos.map(todo => (
              <li key={todo.id}>
                {todo.completed ?
                  <s>{todo.text}</s> :
                  todo.text
                }
              </li>
            ))}
          </ul>
        </div>
      );
    }
    
    

    跳過組件的重新渲染

    在某些情況下,useMemo 也可以幫助你優化子組件重新渲染的效能。為了說明這一點,假設這個 TodoList 組件將 visibleTodos 作為 prop 傳遞給子組件 List 組件

    export default function TodoList({ todos, tab, theme }) {
    // ...
    return (
    <div className={theme}>
    <List items={visibleTodos} />
    </div>
    );
    }

    你可能注意到切換 theme 屬性會讓應用程式凍結一下,但如果從 JSX 中移除 <List />,就會感覺很快。這告訴你值得嘗試優化 List 元件。

    預設情況下,當一個元件重新渲染時,React 會遞迴地重新渲染其所有子元件。這就是為什麼當 TodoList 使用不同的 theme 重新渲染時,List 元件*也會*重新渲染。對於不需要太多計算即可重新渲染的元件來說,這樣沒問題。但是,如果你已驗證重新渲染速度很慢,則可以透過將 List 包裝在 memo 中,來告訴 List 當其屬性與上次渲染時相同則跳過重新渲染:

    import { memo } from 'react';

    const List = memo(function List({ items }) {
    // ...
    });

    透過這個更改,如果 List 的所有屬性都與上次渲染時*相同*,它將會跳過重新渲染。這就是快取計算變得重要的地方!想像一下,你在沒有使用 useMemo 的情況下計算了 visibleTodos

    export default function TodoList({ todos, tab, theme }) {
    // Every time the theme changes, this will be a different array...
    const visibleTodos = filterTodos(todos, tab);
    return (
    <div className={theme}>
    {/* ... so List's props will never be the same, and it will re-render every time */}
    <List items={visibleTodos} />
    </div>
    );
    }

    在上面的例子中,filterTodos 函式總是會建立一個*不同的*陣列,類似於 {} 物件字面量總是會建立一個新的物件。通常,這不會造成問題,但這表示 List 屬性永遠不會相同,並且你的 memo 優化將無法作用。這就是 useMemo 派上用場的地方

    export default function TodoList({ todos, tab, theme }) {
    // Tell React to cache your calculation between re-renders...
    const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab] // ...so as long as these dependencies don't change...
    );
    return (
    <div className={theme}>
    {/* ...List will receive the same props and can skip re-rendering */}
    <List items={visibleTodos} />
    </div>
    );
    }

    透過將 visibleTodos 計算包裝在 useMemo 中,你可以確保它在重新渲染之間具有*相同*的值(直到依賴項發生變化)。除非你有特殊原因,否則你*不必*將計算包裝在 useMemo 中。在此範例中,原因是你將它傳遞給包裝在 memo 中的元件,這讓它可以跳過重新渲染。還有其他一些新增 useMemo 的原因,在本頁面稍後會說明。

    深入探討

    記憶個別 JSX 節點

    你可以將 <List /> JSX 節點本身包裝在 useMemo 中,而不是將 List 包裝在 memo

    export default function TodoList({ todos, tab, theme }) {
    const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
    const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos]);
    return (
    <div className={theme}>
    {children}
    </div>
    );
    }

    行為將會相同。如果 visibleTodos 沒有更改,則 List 將不會重新渲染。

    <List items={visibleTodos} /> 這樣的 JSX 節點是一個像 { type: List, props: { items: visibleTodos } } 的物件。建立這個物件的成本很低,但 React 不知道其內容是否與上次相同。這就是為什麼預設情況下,React 會重新渲染 List 元件。

    但是,如果 React 看到與先前渲染期間完全相同的 JSX,它將不會嘗試重新渲染你的元件。這是因為 JSX 節點是不可變的。JSX 節點物件不會隨著時間改變,因此 React 知道可以安全地跳過重新渲染。但是,要使其運作,節點必須*實際上是同一個物件*,而不僅僅是在程式碼中看起來相同。這就是 useMemo 在此範例中所做的事情。

    手動將 JSX 節點包裝到 useMemo 中並不方便。例如,你無法有條件地執行此操作。這就是為什麼你通常會使用 memo 包裝元件,而不是包裝 JSX 節點的原因。

    跳過重新渲染與始終重新渲染之間的差異

    範例 1說明 2:
    使用 useMemomemo 跳過重新渲染

    在此範例中,List 元件被人為地減慢速度,以便你可以看到當你正在渲染的 React 元件確實很慢時會發生什麼情況。嘗試切換分頁和主題。

    切換分頁感覺很慢,因為它會強制減慢速度的 List 重新渲染。這是預期之內的,因為 tab 已更改,因此你需要在螢幕上反映使用者新的選擇。

    接下來,嘗試切換主題。由於使用了 useMemo 以及 memo,即使人為地降低速度,它仍然很快! List 跳過了重新渲染,因為自上次渲染以來,visibleTodos 陣列沒有改變。 visibleTodos 陣列沒有改變,因為 todostab(您將它們作為依賴項傳遞給 useMemo)自上次渲染以來都沒有改變。

    import { useMemo } from 'react';
    import List from './List.js';
    import { filterTodos } from './utils.js'
    
    export default function TodoList({ todos, theme, tab }) {
      const visibleTodos = useMemo(
        () => filterTodos(todos, tab),
        [todos, tab]
      );
      return (
        <div className={theme}>
          <p><b>Note: <code>List</code> is artificially slowed down!</b></p>
          <List items={visibleTodos} />
        </div>
      );
    }
    
    

    防止 Effect 過於頻繁地觸發

    有時,您可能希望在 Effect 內使用一個值:

    function ChatRoom({ roomId }) {
    const [message, setMessage] = useState('');

    const options = {
    serverUrl: 'https://127.0.0.1:1234',
    roomId: roomId
    }

    useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    // ...

    這會產生一個問題。 每個反應值都必須聲明為 Effect 的依賴項。 但是,如果您將 options 宣告為依賴項,它將導致您的 Effect 不斷重新連接到聊天室

    useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
    }, [options]); // 🔴 Problem: This dependency changes on every render
    // ...

    要解決此問題,您可以將需要從 Effect 中呼叫的物件包裝在 useMemo

    function ChatRoom({ roomId }) {
    const [message, setMessage] = useState('');

    const options = useMemo(() => {
    return {
    serverUrl: 'https://127.0.0.1:1234',
    roomId: roomId
    };
    }, [roomId]); // ✅ Only changes when roomId changes

    useEffect(() => {
    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
    }, [options]); // ✅ Only changes when createOptions changes
    // ...

    這可確保如果 useMemo 返回快取的物件,則 options 物件在重新渲染之間保持相同。

    然而,由於 useMemo 是效能優化,而不是語義保證,如果 有特定原因需要這樣做,React可能會丟棄快取的值。這也會導致 effect 重新觸發,因此更好的做法是通過將您的物件移動到 Effect *內部* 來消除對函數依賴項的需求

    function ChatRoom({ roomId }) {
    const [message, setMessage] = useState('');

    useEffect(() => {
    const options = { // ✅ No need for useMemo or object dependencies!
    serverUrl: 'https://127.0.0.1:1234',
    roomId: roomId
    }

    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
    }, [roomId]); // ✅ Only changes when roomId changes
    // ...

    現在您的程式碼更簡單,而且不需要 useMemo瞭解更多關於移除 Effect 依賴項的資訊。

    記憶體化另一個 Hook 的依賴項

    假設您有一個計算,它依賴於直接在組件主體中創建的物件

    function Dropdown({ allItems, text }) {
    const searchOptions = { matchMode: 'whole-word', text };

    const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
    }, [allItems, searchOptions]); // 🚩 Caution: Dependency on an object created in the component body
    // ...

    依賴像這樣的物件會失去記憶體化的意義。當組件重新渲染時,組件主體內的所有程式碼都會再次運行。創建 searchOptions 物件的程式碼行也將在每次重新渲染時運行。 由於 searchOptions 是您的 useMemo 呼叫的依賴項,而且它每次都不同,React 知道依賴項不同,並且每次都會重新計算 searchItems

    要解決此問題,您可以在將其作為依賴項傳遞之前,先將 searchOptions 物件*本身*記憶體化

    function Dropdown({ allItems, text }) {
    const searchOptions = useMemo(() => {
    return { matchMode: 'whole-word', text };
    }, [text]); // ✅ Only changes when text changes

    const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
    }, [allItems, searchOptions]); // ✅ Only changes when allItems or searchOptions changes
    // ...

    在上面的範例中,如果 text 沒有改變,則 searchOptions 物件也不會改變。但是,更好的解決方案是將 searchOptions 物件宣告移動到 useMemo 計算函數的*內部*

    function Dropdown({ allItems, text }) {
    const visibleItems = useMemo(() => {
    const searchOptions = { matchMode: 'whole-word', text };
    return searchItems(allItems, searchOptions);
    }, [allItems, text]); // ✅ Only changes when allItems or text changes
    // ...

    現在您的計算直接依賴於 text(它是一個字串,不會“意外地”變得不同)。


    記憶體化函數

    假設 Form 組件被 memo 包裹。您想要將一個函數作為 prop 傳遞給它

    export default function ProductPage({ productId, referrer }) {
    function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
    referrer,
    orderDetails
    });
    }

    return <Form onSubmit={handleSubmit} />;
    }

    就像 {} 創建一個不同的物件一樣,函數宣告,例如 function() {} 和表達式,例如 () => {} ,在每次重新渲染時都會產生一個*不同*的函數。就其本身而言,創建一個新函數並不是問題。這不是需要避免的事情!但是,如果 Form 組件被記憶體化,則假設您希望在沒有 prop 改變時跳過重新渲染它。一個*總是*不同的 prop 會失去記憶體化的意義。

    要使用 useMemo 記憶體化函數,您的計算函數必須返回另一個函數

    export default function Page({ productId, referrer }) {
    const handleSubmit = useMemo(() => {
    return (orderDetails) => {
    post('/product/' + productId + '/buy', {
    referrer,
    orderDetails
    });
    };
    }, [productId, referrer]);

    return <Form onSubmit={handleSubmit} />;
    }

    這看起來很笨拙!記憶體化函數很常見,以至於 React 有一個專門用於此的內建 Hook。將您的函數包裝到 useCallback 中,而不是 useMemo 中,以避免必須編寫額外的巢狀函數

    export default function Page({ productId, referrer }) {
    const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
    referrer,
    orderDetails
    });
    }, [productId, referrer]);

    return <Form onSubmit={handleSubmit} />;
    }

    上面兩個範例完全相同。 useCallback 的唯一好處是它可以讓您避免在內部編寫額外的巢狀函數。它沒有做任何其他事情。 閱讀更多關於 useCallback 的資訊。


    疑難排解

    我的計算在每次重新渲染時都會執行兩次

    嚴格模式中,React 會呼叫您的某些函式兩次而不是一次。

    function TodoList({ todos, tab }) {
    // This component function will run twice for every render.

    const visibleTodos = useMemo(() => {
    // This calculation will run twice if any of the dependencies change.
    return filterTodos(todos, tab);
    }, [todos, tab]);

    // ...

    這是預期的行為,而且不應該會破壞您的程式碼。

    這種僅限開發環境的行為可以幫助您保持元件的純粹性。React 會使用其中一次呼叫的結果,並忽略另一次呼叫的結果。只要您的元件和計算函式是純粹的,這就不應該會影響您的邏輯。但是,如果它們不小心不純粹,這可以幫助您注意到並修正錯誤。

    例如,這個不純粹的計算函式會改變您作為 prop 接收的陣列

    const visibleTodos = useMemo(() => {
    // 🚩 Mistake: mutating a prop
    todos.push({ id: 'last', text: 'Go for a walk!' });
    const filtered = filterTodos(todos, tab);
    return filtered;
    }, [todos, tab]);

    React 會呼叫您的函式兩次,因此您會注意到待辦事項被添加了兩次。您的計算不應該更改任何現有的物件,但可以更改您在計算過程中建立的任何*新*物件。例如,如果 filterTodos 函式總是返回一個*不同*的陣列,您可以改為改變*那個*陣列

    const visibleTodos = useMemo(() => {
    const filtered = filterTodos(todos, tab);
    // ✅ Correct: mutating an object you created during the calculation
    filtered.push({ id: 'last', text: 'Go for a walk!' });
    return filtered;
    }, [todos, tab]);

    閱讀保持元件的純粹性以瞭解更多關於純粹性的資訊。

    另請參閱關於更新物件更新陣列而不進行變更的指南。


    我的 useMemo 呼叫應該返回一個物件,但返回 undefined

    這段程式碼無法運作

    // 🔴 You can't return an object from an arrow function with () => {
    const searchOptions = useMemo(() => {
    matchMode: 'whole-word',
    text: text
    }, [text]);

    在 JavaScript 中,() => { 開始箭頭函式的函式主體,所以 { 大括號不是您物件的一部分。這就是它沒有返回物件的原因,並且導致錯誤。您可以透過添加括號來修復它,例如 ({})

    // This works, but is easy for someone to break again
    const searchOptions = useMemo(() => ({
    matchMode: 'whole-word',
    text: text
    }), [text]);

    然而,這仍然令人困惑,而且很容易讓其他人透過移除括號來破壞它。

    為了避免這個錯誤,請明確地寫一個 return 陳述式

    // ✅ This works and is explicit
    const searchOptions = useMemo(() => {
    return {
    matchMode: 'whole-word',
    text: text
    };
    }, [text]);

    每次我的元件渲染時,useMemo 中的計算都會重新執行

    請確保您已將依賴陣列指定為第二個參數!

    如果您忘記依賴陣列,useMemo 每次都會重新執行計算

    function TodoList({ todos, tab }) {
    // 🔴 Recalculates every time: no dependency array
    const visibleTodos = useMemo(() => filterTodos(todos, tab));
    // ...

    這是將依賴陣列作為第二個參數傳遞的修正版本

    function TodoList({ todos, tab }) {
    // ✅ Does not recalculate unnecessarily
    const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
    // ...

    如果這沒有幫助,則問題是至少有一個依賴項與先前的渲染不同。您可以透過手動將您的依賴項記錄到主控台來除錯此問題

    const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
    console.log([todos, tab]);

    然後,您可以在主控台中右鍵點擊來自不同重新渲染的陣列,並為它們選擇「儲存為全域變數」。假設第一個被儲存為 temp1,第二個被儲存為 temp2,那麼您可以使用瀏覽器主控台來檢查兩個陣列中的每個依賴項是否相同

    Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
    Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
    Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

    當您找到哪個依賴項破壞了記憶體時,請找到移除它的方法,或者也將其記憶體化


    我需要在迴圈中為每個清單項目呼叫 useMemo,但不允許這樣做

    假設 Chart 元件被 memo 包裹。當 ReportList 元件重新渲染時,您想要略過清單中每個 Chart 的重新渲染。但是,您不能在迴圈中呼叫 useMemo

    function ReportList({ items }) {
    return (
    <article>
    {items.map(item => {
    // 🔴 You can't call useMemo in a loop like this:
    const data = useMemo(() => calculateReport(item), [item]);
    return (
    <figure key={item.id}>
    <Chart data={data} />
    </figure>
    );
    })}
    </article>
    );
    }

    相反,請為每個項目提取一個元件,並記憶體化個別項目的資料

    function ReportList({ items }) {
    return (
    <article>
    {items.map(item =>
    <Report key={item.id} item={item} />
    )}
    </article>
    );
    }

    function Report({ item }) {
    // ✅ Call useMemo at the top level:
    const data = useMemo(() => calculateReport(item), [item]);
    return (
    <figure>
    <Chart data={data} />
    </figure>
    );
    }

    或者,您可以移除 useMemo,而是將 Report 本身包裹在 memo 中。如果 item prop 沒有改變,Report 將會略過重新渲染,因此 Chart 也將會略過重新渲染

    function ReportList({ items }) {
    // ...
    }

    const Report = memo(function Report({ item }) {
    const data = calculateReport(item);
    return (
    <figure>
    <Chart data={data} />
    </figure>
    );
    });