# [React] `useCallback` 記憶函式的手段,避免 re-render 重新建立新的函式
###### tags: `React` `前端筆記` `Udemy 課程筆記`
`useCallback` 針對函式使用,`useCallback` 包另一個函式,並記憶它。
> 被記憶的是 `useCallback` 的第一個參數(function)
`useCallback(() => { callback(a, b); }, [dep],);`
- `useCallback(callback)` -> 沒給 dependency array,代表每次 re-render 時都會重新建立 callback = 沒用 XD
- `useCallback(callback, [])` -> 給空陣列,永遠不會重新建立 callback,因為每次 re-render dependency 都是空陣列,並不會改變
- `useCallback(callback, [values])` -> 每次 re-render 時 React 都會檢查 dependency array 內的值是否有改變,如果有的話就會重新建立該 callback,否則就會直接回傳被記憶的 callback
>==Functions are closures, which means they close over the values(variables) that are available in their environment.==
>不單只是保存函式而已,連該函式的變數讀取範圍都會保存 -> 保存該函式的 scope
## 使用 `useCallback() 時別忘了 closure`
以課程的範例可以知道理解 closure 的重要性:
`isTestActive` 為 `true` 時才可以變更 `isActive`,但不幸的是,儘管已經透過點擊事件,將 `isTestActive` 變成 `true` 了,但是我們還是沒辦法觸發另一個按鈕更新 `isActive` 的值。
```javascript=
// src/app.js
// ...
export default function App() {
console.log("app render");
const [isActive, setIsActive] = useState(false);
const [isTestActive, setIsTestActive] = useState(false);
/*
dependency array 給 [] -> 該 callback 永遠不會改變,
因為每次 re-render 時 [] 不會有任何的變更
*/
const clickHandler = useCallback(() => {
if (isTestActive) {
setIsActive((prevState) => !prevState);
}
// 當心!這裡會出現問題!
}, []);
const clickTestHandler = () => {
setIsTestActive((prevState) => !prevState);
};
return (
<div className="App">
<Button onToggleActive={clickHandler}>Click me</Button>
<Button onToggleActive={clickTestHandler}>Click me test</Button>
{/*把 props 寫死,即可看到 Demo component 不會重新 render*/}
<Demo isActive={isActive} />
</div>
);
}
```
### 因為給了 `[]` 當作 dependency,所以沒有機會告訴 React 要重新建立函式
所有的函式就時閉包,所以當開發者使用 `useCallback(callback, deps)` 告知 React 要記憶 callback 時,其實 React 也連帶記憶了該 callback 的所有變數讀取範圍。
```javascript=
// src/app.js
// ...
const [isActive, setIsActive] = useState(false);
const [isTestActive, setIsTestActive] = useState(false);
/*
dependency array 給 [] -> 該 callback 永遠不會改變,
因為每次 re-render 時 [] 不會有任何的變更
*/
const clickHandler = useCallback(() => {
if (isTestActive) {
setIsActive((prevState) => !prevState);
}
// 當心!這裡會出現問題!
}, []);
// ...
```
以課程的例子來看:
- STEP 1:初次 render 時 `isTestActive` 為 `false`
- STEP 2:程式碼執行至 `useCallback`,`useCallback` 內的 callback 的 `if(isTestActive)` 會往外層的 scope 抓,就會抓到初始值 `false`,且使用 `[]` 告知 React:永遠不要重新建立這個 callback(因為不管怎麼 re-render `[]` 都是 `[]`)
- STEP 3:使用者點擊按鈕更新 `isTestActive`
- STEP 4:即便 `isTestActive` 已經更新,但是 `useCallback(callback)` 所記憶的是初次的 callback,所以對這個 callback 來說 `isTestActive` 永遠都是 `false`
### 解決的辦法 -> 塞 value 至 dependency array 內,告訴 React 這些值有變動的話就隨著 re-render 重新建立函式並記憶
state update -> re-render -> 執行到 `useCallback()` React 發現 dependency 有變動 -> 創建新的函式並記憶
```javascript=
// src/app.js
// ...
const clickHandler = useCallback(() => {
if (isTestActive) {
setIsActive((prevState) => !prevState);
}
// 告知 React 這裡的值有變的話就隨著 re-render 重新建立這個函式並記憶
}, [isTestActive]);
// ...
```
[完整程式碼範例](https://codesandbox.io/s/udemy-react-section-157-usecallback-et4np9?file=/src/App.js)
## Recap
1. `useCallback(callback, dep)` 用來告知 React 記憶某個 callback
2. `useCallback(callback, ded)` 不僅記憶 callback 還會記憶其 callback 的 scope -> closure 的觀念
3. 搭配 `React.memo` 可以幫助 `React.memo` 比對 poitner function
4. 不會叫用包覆的 callback,只會回傳(`useCallback` does not invoke its wrapped function & returns that wrapped function.)
5. 只有當 `[ded]` 有改變時,`useCallback` 才會建立一個新的函式
6. `[ded]` 的比較是使用 `Object.is()` 判斷,可以想成 `===` 絕對相等的比較
## 參考資料
1. [React - The Complete Guide (incl Hooks, React Router, Redux) - sec. 157](https://www.udemy.com/course/react-the-complete-guide-incl-redux/learn/lecture/25599610#questions)
2. [Your Guide to React.useCallback()](https://dmitripavlutin.com/react-usecallback/)
3. [Day9-React Hook 篇-認識 useCallback](https://ithelp.ithome.com.tw/articles/10270317)