因為面試被問倒 useCallback
、useMemo
的用途和用法,還有如何避免 React 做無謂的重新渲染 (re-render),特別是最後一個當下我完全沒有頭緒…
小提示:使用
React.memo
,整理筆記發現有記過這個,但看來真的只是寫下來 😅
事後亡羊補牢時看到 Debug Bear 的這篇文章;文章中漸進式的說明與例子終於讓我比較看懂這三個方法對重新渲染的影響,另外也提到了 list 元素中,React 會提示需要提供給元件的 key
屬性也是影響是否重新渲染的因子之一。
我參考文章中的例子,寫了個應該算是互動式的應用範例:
範例中因為版面有限,主要透過題目中的簡短說明,期望使用者配合閱讀程式碼,及程式碼中設定 console.log
印出的資訊來瞭解這四個主題 – 總共有 9 個問題。
這篇文章則是進一步描述每個主題的癥結點,提取出關鍵的程式碼部份做說明 (因此會與實際範例中的程式碼有差異),但不會細部的去分析這些方法原始碼的運作概念 (因為我也還不懂)。
React.memo
問題 1 及問題 2 是針對 React.memo
的應用,
// @filename: Question1.tsx
export default function Question1() {
const [answer, setAnswer] = useState<boolean | null>(null);
return <Selection setAnswer={setAnswer} />
}
因為 Selection
有設定 onClick
會觸發 setAnswer
改變 Question1 元件 answer
的值,因此造成整個元件重新更新。
透過使用 React.memo
當 Selection
沒有特別的改變時,比如從母元件接收到的 props 沒有變化,React 就不會重新渲染它,也就是 Question2 想展示的:
// @filename: Question2.tsx
const Selection = React.memo(function Selection({setAnswer}) {
// ...
});
useCallback
問題 3 及問題 4 是針對 useCallback
的應用,主要的變化在給 Selection
這個元件 setAnswer
的值的變化,變成了給 arrow function 而不是在 React 管理下的 setState 函式;儘管有使用 React.memo
,但每次 Question3
狀態改變就會產生一個新的 arrow function 給 Selection
也因此會被 React 判斷為元件拿到的 props 有更新,進而有重新渲染。
// @filename: Question3.tsx
export default function Question3() {
return <Selection setAnswer={(ans) => setAnswer(ans)} />
}
而這邊的解法,就是用 useCallback
把這個 arrow function 的輪廓記起來:
// @filename: Question4.tsx
export default function Question4() {
return <Selection setAnswer={useCallback((ans) => setAnswer(ans), [setAnswer])} />
}
注意,useCallback
的第二個參數是一個陣列,可以給變數做為 dependencies,當 dependencies 中有任一值改變時,就會觸發重新渲染,這邊選用 Question4
的 setAnswer
做為是否重新渲染的依賴。
useMemo
問題 5 和問題 6 是針對 useMemo
的應用,這兩個例子中 Question 元件中有個 childStyle
物件會做為 props 傳給 Selection
元件做為最外層 div
元素的 inline style 的 CSS 屬性設定使用;並且裡面屬性的值會依據 Question 中 state 值有所不同,
// @filename: Question5.tsx
export default function Question5() {
const [answer, setAnswer] = useState<boolean | null>(null);
const childStyle = {
fontWeight: answer ? '500' : '700',
color: answer ? '#fff' : 'cyan',
};
return <Selection style={childStyle} setAnswer={setAnswer} />
}
const Selection = React.memo(function Selection({style, setAnswer}) {
return (
<div style={style}>
{/*...*/}
</div>
)
});
這裡的問題與 useCallback
該節中提到的一樣,因為 Question5
的 answer
狀態被子元件觸發 setAnswer
給改變而重新渲染,現在 React 認為 childStyle
不一樣了 (因為是新造的)。
我們可以使用 useMemo
來記住這個物件,並且跟 useCallback
一樣指定 dependencies:
// @filename: Question6.tsx
export default function Question6() {
const [answer, setAnswer] = useState<boolean | null>(null);
const childStyle = useMemo(() => ({
fontWeight: answer ? '500' : '700',
color: answer ? '#fff' : 'salmon',
}),
[setAnswer]
);
}
可以注意到,因為使用 setAnswer
做為 dependencies,childStyle
並不會在 Question6
重新渲染時被改變,雖然子元件 Selection
沒有被重新渲染了,但也因此 inline style 沒有改變,這可能不是好的 UX 表現,函式的用法也比較不合理。
開頭提到來自 Debug Bear,文章的這個段落所寫的範例們是應用上更為合理的,我將 childStyle
放在 Question 元件的 scope 內,以及使用 setAnswer
做為 useMemo
的 dependency 都只是為了保持題目結構上的一致性。
List Elements’ Key Property
問題 7 到問題 9 是針對 list 元素中,key
這個 props 的作用,
// @filename: Question7.tsx
const descriptions = [
{
name: 'foo',
content: '母元件透過陣列產生 list 元素,沒有綁 key 屬性',
},
{
name: 'bar',
content: '點選回答會讓 list 元素的順序調換',
},
];
export default function Question7() {
const [items, setItems] = useState(descriptions)
return (
<ul>
{items.map((item) => (
<ListItem item={item} />
))}
</ul>
)
}
const ListItem = React.memo(function ListItem({item}){
return <li>{item.content}</li>
});
首先 console 就會跳錯誤說透過 map
產出 ListItem
元素需要加入 key
屬性幫助 React 辨識,所以不意外的 Question7
重新渲染後 ListItem
也跟著重新渲染了。
那麼在問題 8 我們加入 key
這個屬性,用的是 map
會自帶的 index
參數,
export default function Question7() {
const [items, setItems] = useState(descriptions)
return (
<ul>
{items.map((item, index) => (
<ListItem key={index} item={item} />
))}
</ul>
)
}
然後子元件 Selection
還是會重渲染;我這邊的操作是反轉陣列中物件的順序,index
並非唯一獨特的值,而是 0 和 1,反轉後原本 name: "foo"
的物件變到 index = 1 的位置,name: "bar"
則變到了 index = 0 的位置;React 官網中有提到這會產生非預期的表現,比如這裡,就被當成了兩個與原先不同的子元件,觸發了子元件的重新渲染。
這樣講或許不好懂,所以來看問題 9,用對比的方式會比較清楚是否為唯一獨特的 key
產生的差異;將物件中的 name
屬性做為 key
,現在它們是:
export default function Question7() {
const [items, setItems] = useState(descriptions)
return (
<ul>
{items.map((item) => (
<ListItem key={item.name} item={item} />
))}
</ul>
)
}
可以看一下 console.log
印出的狀況,應該可以發現子元件 Selection
就算觸發了順序調換,也沒有被重新渲染了。
題外話:動態載入元件 (lazy loading)
為了動態的切換元件,我參考了 Digital Ocean 的這篇文章,也學到了怎麼用 React 的 lazy
配合 Suspend
元件,做 lazy loading。
Joe Chang 在 Medium 上的這篇文章也不錯,比較簡單扼要,而且是中文的。
文章中提到
lazy
必須搭配Suspend
使用,不然會報錯,另外Suspend
元件需要fallback
屬性指定使用lazy
的元件載入完成前做為備案顯示的元件。
Aleksandr Hovhannisyan 的這篇文章 有把 Vanilla JS 中的 lazy import 的做法寫出來,另外也提到了:
配合 React 的
lazy
必須進行動態載入的元件,必須是能使用import default
引入的。 (但 Vanilla JS 的做法中使允許使用 named import 的)。