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