# [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)