(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
在前面,我們說在非必要的時候,不要在React function component內宣告函式。
那什麼時候是「必要的時候」呢 ?
舉例來說,我們前面有提過可以用useRef當作counter,這個時候如果我們希望透過MenuItem被點擊的時候,印出counter的值,就要這樣寫:
import React, {useState,useRef} from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';
let menuItemWording=[
"Like的發問",
"Like的回答",
"Like的文章",
"Like的留言"
];
const MenuPage = () =>{
const [isOpen, setIsOpen] = useState(true);
/* 定義counter */
const renderCounter = useRef(0);
renderCounter.current++;
/* 定義列印函式 */
const handleClick = () => {
console.log("counter is " + renderCounter.current);
}
/* 綁定函式到元素上 */
let menuItemArr = menuItemWording.map((wording) =>{
return <MenuItem text={wording} handleClick={handleClick}/>
});
return (
<OpenContext.Provider value={{
openContext: isOpen,
setOpenContext: setIsOpen
}} >
<Menu title={"Andy Chang的like"}>
{menuItemArr}
</Menu>
</OpenContext.Provider>
);
}
export default MenuPage;
import React, {memo, useContext} from 'react';
import { OpenContext } from '../context/ControlContext';
const menuItemStyle = {
marginBottom: "7px",
paddingLeft: "26px",
listStyle: "none"
};
function MenuItem(props){
//const isOpenUtil = useContext(OpenContext);
//const func = isOpenUtil;
/* 綁定handleClick */
return <li
style={menuItemStyle}
onClick={()=>{props.handleClick()}}
>
{props.text}
</li>;
}
export default memo(MenuItem);
理論上因為我們的props中wording沒有變,handleClick也因為牽涉的變數使用了useRef
,定義也沒有變。根據上一篇我們對於memo
的解說,此時memo
會幫助我們不重新渲染元件。
然而你打開profiler,卻會看到以下結果:
為什麼會這樣?
這是因為memo在比較props的時候,當遇到物件類別,會去比較它的reference,而不是一一比對物件當中的屬性。前面提過,當function component被重新渲染時會呼叫整個元件函式的定義域。由於函式也是物件的一種,這裡MenuPage的handleClick
在重新渲染時也被重新宣告了一次,導致reference改變,MenuItem的memo就會覺得props被改變了。
useCallback
就是能夠用來解決這件事情的React hook。它很像是「專為函式定義用的useRef
」,可以幫我們確保函式非必要時不會被重新定義,也就是reference不變。
它的語法跟useEffect很像:
const func = useCallback(定義函式,[相依變數]);
舉例來說,剛剛的MenuPage只要改成這樣,MenuItem就不會被重新渲染:
import React, {useState, useRef, useCallback} from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';
let menuItemWording=[
"Like的發問",
"Like的回答",
"Like的文章",
"Like的留言"
];
const MenuPage = () =>{
const [isOpen, setIsOpen] = useState(true);
const renderCounter = useRef(0);
renderCounter.current++;
const handleClick = useCallback(() => {
console.log("counter is " + renderCounter.current);
},[renderCounter]);
let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording} handleClick={handleClick}/>);
return (
<OpenContext.Provider value={{
openContext: isOpen,
setOpenContext: setIsOpen
}} >
<Menu title={"Andy Chang的like"}>
{menuItemArr}
</Menu>
</OpenContext.Provider>
);
}
export default MenuPage;
但是這樣useCallback
和useRef
的差別在哪裡呢?
還記得剛剛介紹語法時的第二個參數「相依變數」嗎? useCallback
可以讓我們用這個第二個參數,來設定哪些東西被改變時,要去重新定義函式。
以下面的例子來說,如果我們把handleClick改成印出isOpen
的值:
const handleClick = useRef(() => {
console.log("isOpen is " + isOpen);
});
你會發現上面這個函式永遠只會印出isOpen
在第一次渲染的初始值。這是因為isOpen的值在定義函式時,原始值就被填進去了,之後由於我們沒有去特別更新useRef的ref.current,所以定義始終沒有被改變。
這個時候改成使用useCallback就會像這樣:
const handleClick = useCallback(() => {
console.log("isOpen is " + isOpen);
},[isOpen]);
此時你就會發現函式印出來的值跟isOpen一樣了。不過也因為定義被改變了,這個時候MenuItem的重新渲染就不可避免。
和useEffect一樣,React會希望useCallback中定義的內容只和第二個參數中有被定義的東西有關。當你在useCallback中使用了state、props、React hook相關的東西而又沒有定義在第二個參數時,React也會跳warning。