# [React] `useMemo` 記憶昂貴的運算結果,避免 re-render 時重新執行複雜的運算 ###### tags: `React` `前端筆記` `Udemy 課程筆記` 如果想要避免每次 re-render 都重複進行複雜的運算,就可以用 `useMemo()` 告訴 React 在某些情況中在重新計算,其餘皆直接拿複雜運算的結果即可(就不用重新執行複雜的運算了)。 `useMemo(() => computeExpensiveValue(a, b), [a, b]);` > 被記憶的是「複雜運算的結果」,所以第一個參數必須回傳運算結果,一樣是使用 dependency array 告知 React 是否需要重新計算,還是直接拿上次的結果。 ```javascript // 上方的語法是十分精簡版: /* useMemo(pure function, [ded]),第一個參數的函式必須要回傳值,當作日後 [ded] 未變更時省略計算直接回傳的值 */ const memoizedValue = useMemo(() => { const result = computeExpensiveValue(a, b); return result }, [a, b]) ``` ## dependency array 常塞「執行複雜運算之函式的參數」 需要的結果是「避免複雜運算」-> 如果結果是一樣的代表不用重算 -> 函式的 input(arguments) 一樣的話,應該就會得到相同的 output(result) -> dependency array 塞 input -> 如果 input 不一樣 = output 也不一樣,所以要重算 - 不給 dependency array -> 每次 re-render 都會重新計算 = 白給 - 給 `[]` -> 只有第一次 render 會計算,之後的 re-render 就不會重新算,因為 `[]` 沒有變的可能性(所以計算的結果就會是一樣的) - `[...someInputs]` -> 除非 `[...someInputs]` 改變了,要不然就不會重新計算 ## 將運算放在 `useMemo()` 內並回傳「結果」,在更改的條件未被觸發時,就不會重新進行運算 ```javascript= // ... const DemoList = (props) => { const { list } = props; const sortedList = list.sort((a, b) => a - b); const testMemo = useMemo(() => { /* 只會在第一 render 時會計算,第二次 re-render 開始就不會重新計算 而是直接拿第一次 render 的結果,因為 dependency array 給 [] */ console.log("in useMemo callback"); return Math.random(); }, []); return ( <> <h1>In DemoList</h1> <h2>{testMemo}</h2> <ul> {sortedList.map((item) => ( <li key={item}>{item}</li> ))} </ul> </> ); }; // ... ``` - STEP 1:第一次 render 回執行這個 component 內的程式碼 - STEP 2:執行至 `useMemo()`,被包覆的計算會被執行,React 會記憶這個運算的結果 - STEP 3:component 被 re-render - STEP 4:component 內的程式碼被重新執行 - STEP 5:執行至 `useMemo()`,因為 dependency array 給 [],所以沒有變更的可能,因此 React 只會回傳該運算上次的結果(所以不會重新執行 `useMemo()` 內的函式) ## 如果 parent component 有將物件型別的 props 鎖死的需求,`useMemo()` 也可以排上用場 ```javascript= // src/app.js // ... export default function App() { console.log("app render"); const [listTitle, setListTitle] = useState("My List"); // const numberList = [3, 2, 1, 9]; /* 如果想要鎖死 parent 傳給 child 的物件型別 props, 也可以用 useMemo() + [] 確保這個物件不會因為 re-render 的緣故重新建立一個新的記憶體 */ const numberList = useMemo(() => [3, 2, 1, 9], []); const clickHandler = () => { setListTitle("New Title"); }; return ( <div className="App"> <h2>{listTitle}</h2> <button onClick={clickHandler}>Change title</button> <DemoList list={numberList} /> </div> ); } ``` ```javascript= // src/components/DemoList.js // ... const DemoList = (props) => { console.log("demo list render"); const { list } = props; // const sortedList = list.sort((a, b) => a - b); /* 當不滿足 useMemo() 重新計算之條件時,比不會重新計算(因此就看不到 console.log) 第一次 render 會完整執行計算,第二次 re-render 開始會查看 dependency array 內的 value 是否有變動,有才會觸發重新計算 */ const sortedList = useMemo(() => { console.log("in sorted list"); return list.sort((a, b) => a - b); }, [list]); const testMemo = useMemo(() => { /* 只會在第一 render 時會計算,第二次 re-render 開始就不會重新計算 而是直接拿第一次 render 的結果,因為 dependency array 給 [] */ console.log("in useMemo callback"); return Math.random(); }, []); return ( <> <h1>In DemoList</h1> <h2>{testMemo}</h2> <ul> {sortedList.map((item) => ( <li key={item}>{item}</li> ))} </ul> </> ); }; // ... ``` 執行運行上方的程式碼可以發現在第一次 render 後便不再執行 `sortedList` 及 `testMemo` 的計算了,而是直接拿第一次 render 的結果,因為: > 1. `sortedList`,有開 dependency array 觀察 `[list]` 有無改變,但因為 parent component 用了 `useMemo()` 鎖死回傳的值(也就是傳下去 `list` ,因此 `sortedList` 觀察的 `[list]` 永遠都不會變 > 2. `testMemo` 用了 `[]` 鎖死,所以並不會觸發重新計算 [完整程式碼範例](https://codesandbox.io/s/udemy-react-section-161-usememo-5j4ngc?file=/src/App.js) ## Recap 1. `useMemo()` 記錄運算的「結果」,有達到重新計算的條件才會重新計算,要不然就拿記錄的結果 2. `useMemo()` 僅和「是否重新運算有關」,不會影響「是否要重新 re-render」 ## 參考資料 1. [React - The Complete Guide (incl Hooks, React Router, Redux)](https://www.udemy.com/course/react-the-complete-guide-incl-redux/learn/lecture/25599630#questions) 2. [React 性能優化那件大事,使用 memo、useCallback、useMemo](https://medium.com/%E6%89%8B%E5%AF%AB%E7%AD%86%E8%A8%98/react-optimize-performance-using-memo-usecallback-usememo-a76b6b272df3) -> 裡面還有關於 `useCallback` 及 `React.memo` 的例子 3. [Day8-React Hook 篇-認識 useMemo](https://ithelp.ithome.com.tw/articles/10269673)