昨天我們介紹了 useMemo,這個 hook 可以幫助我們記憶依賴參數,當參數變化時才會重新執行函式,並且取得函式回傳的值。
今天要來介紹的是 useCallback,跟 useMemo 相似,都是記憶依賴參數,變化時再重新執行,但回傳值及使用方式上又所不同。
const someFn = (a, b)=>{...}
const getValue = useCallback(()=>someFn(a, b), [a, b])
跟 useMemo 一樣,都是在第一個參數中放入一個 callback function,並且在第二個參數中放入依賴參數:一個含要偵測變數的 Array,當 Array 中的值發生變化時,就會執行 callback function。
useCallback 與 useMemo 相同,都是為了避免 React 重複不必要的渲染或執行、提升 React 的效能為目的設計的。但 useCallback 是回傳 function, useMemo 會回傳 callback function 執行後的值,而 useCallback 則是會回傳整個傳入的 function。
useCallback 的主要目的在於避免 function 因為元件的重新渲染而不斷的重新建立,造成不必要的重新渲染。
前天有介紹過 React.memo,這是一個會比較元件前後的 props 有無不同,若有變化再行渲染的一個 HOC,但若是 props 是物件,那 React.memo 就沒轍了。
為了解決這個問題,可以搭配使用 useCallback。
例如有一個傳入 function 當 props 的元件:
const Child = memo(({fn})=>{
const counter = useRef(0);
useEffect(()=>{
counter.current++;
});
return (
<div>
<p>answer: {fn()}</p>
<p>Child 更新 {counter.current} 次</p>
</div>
)
})
const Parent = () =>{
const [count, setCount] = useState(0);
const a = 1;
const b = 2;
const myFunction = ()=>{
return a+b
}
return (
<div>
<Child fn={myFunction}/>
<button onClick={()=> setCount((prevCount)=>prevCount+1)}>Parent 更新 {count} 次</button>
</div>
)
}
可以看到,一個 function 被傳入,當父元件變化時,傳入的 function 完全沒有變化,即使子元件使用了 React.memo,子元件仍然會跟著更新。
要解決這個問題,可以使用 useCallback:
const myFunction = useCallback(()=>{
return a+b
}, []);
這樣子元件就不會重新渲染了。
如果在 function 中的不是單純的變數,而是 state 的話,要怎麼做呢?
一般來說可能會想到這樣:
const [num, setNum] = useState(0);
const myFunction = useCallback(() => {
setNum(num + 1)
}, []);
//...
<Child myFunction={myFunction} num={num}/>
但是這樣做了之後會發現:使用函式一次後, num
值就不再更新了!
會這樣的原因在於 myFunction 取 state 的方式是透過閉包取得同在一個詞法作用域 (lexical scope) 的值,但是在 React 中 state 值是 immutable 的,可以理解為 myFunction 取得 state 後,由於沒有再更新,因此函式內的 state 保持著初始值,自然 state 不會再更新。
要處理這個函式,我們可以用 useState 用 callback function 更新 state 的方法:
const myFunction = useCallback(()=>{
setNum(prevNum => prevNum + 1);
}, [])
useState 會使用前一個 state 的值,而不是使用閉包取得外面的 state ,因此可以正常使用。
由於 useCallback 在渲染時執行,因此可以用作在執行 useEffect 之前的確保,確保 function 中已經先使用了 props/ state:
const Child = ({path}) => {
const getUrl = useCallback((query)=>{
return `https://example.com/${path}?query=${query}`
}, [path])
useEffect(()=>{
const fetchUrl = async()=>{
const data = await fetch(getUrl("js"));
...
}
}, [getUrl])
useEffect(()=>{
const fetchUrl = async()=>{
const data = await fetch(getUrl("python"));
...
}
}, [getUrl])
return (
<div>
...
</div>
)
}
useCallback、useMemo 提供了方法,幫助我們能更好的提升程式效能,讓我們能夠確保我們的網頁能運行的更加快速及順暢。
參考資料
如何錯誤地使用 React hooks useCallback 來保存相同的 function instance
什麼時候該使用 useMemo 跟 useCallback
Day9-React Hook 篇-認識 useCallback
useCallback - Hooks API 參考