這次來聊聊進階一點的話題 memo 的應用,在談論這個話題之前我們先來了解 memo 是什麼?
React memo 是 React 函式組件的高階組件(H.O.C.),用於提高組件的渲染效率,它可以通過記憶組件的渲染結果,避免無效的重新渲染,當組件的 props 不變時,它可以回傳之前渲染的結果而不是重新渲染。
關於高階組件(H.O.C.)的部分,之後的章節會重點介紹到,重點會放在記憶組件的渲染結果部分,怎麼說呢?
下面我用一個母組件嵌套子組件的範例來示範遇到怎樣的問題好了:
import React, { memo, useCallback, useMemo, useState } from 'react'
// 子層接 props 參數,觸發渲染
const Swatch = ({ color }) => {
console.log(`Swatch 渲染 ${color}`);
return (
<div
style={{
margin: '1rem',
width: 75,
height: 75,
borderRadius: '50%',
backgroundColor: color,
}}
></div>
);
};
// 母層
// 預設一開始情況下會發現每次按按鈕都會使子層也重新渲染,
// 即使 props 的值根本沒變
const MemoExample = () => {
const [appRenderIdx, setAppRenderIdx] = useState(0)
console.log(`Memo 渲染次數 ${appRenderIdx}`);
return (
<div>
<h4>React Memo 範例</h4>
<div className='f-b-c'>
<button
onClick={() => setAppRenderIdx(appRenderIdx + 1)}
>
重新渲染
</button>
</div>
{/* default */}
<Swatch color='red' />
</div>
);
};
export default MemoExample
此時你的畫面應該會看到如下圖:
當我們按下按鈕時會發現子層的色塊圓圈也跟著重新渲染了,即使他的 props 根本沒發生變化,如下圖:
想要解決以上的問題可以很簡單的利用 React.memo 多包裝一層在 Swatch 之上,你可以簡單理解為多封包一層來改變傳入的 component 屬性就好,那麼我們繼續來看套用之後的改變,如下:
import React, { memo, useCallback, useMemo, useState } from "react";
// 子層接 props 參數,觸發渲染
const Swatch = ({ color }) => {
console.log(`Swatch 渲染 ${color}`);
return (
<div
style={{
margin: "1rem",
width: 75,
height: 75,
borderRadius: "50%",
backgroundColor: color
}}
></div>
);
};
// 這裡用上memo封裝原本的Swatch
const MemoSwatch = memo(Swatch);
// 母層
const MemoExample = () => {
const [appRenderIdx, setAppRenderIdx] = useState(0);
console.log(`Memo 渲染次數 ${appRenderIdx}`);
return (
<div>
<h4>React Memo 範例</h4>
<div className="f-b-c">
<button onClick={() => setAppRenderIdx(appRenderIdx + 1)}>
重新渲染
</button>
</div>
{/* default */}
<Swatch color="red" />
{/* with memo */}
<MemoSwatch color="blue" />
</div>
);
};
export default MemoExample;
我留了兩個色塊讓大家可以比較容易看到差異,那畫面應該會如下圖:
當按下按鈕時會看到有用React.memo處理的component已經不會因為重新渲染按鈕動作而觸發無意義的重複渲染了,如下圖:
這裡稍微提一下新手常常會弄錯的地方,包含我自己在剛學習使用的時候也時常會搞混,要實現這個範例前我們先加一個更換顏色的功能上去,如下:
import React, { memo, useCallback, useMemo, useState } from "react";
// 子層接 props 參數,觸發渲染
const Swatch = ({ color }) => {
console.log(`Swatch 渲染 ${color}`);
return (
<div
style={{
margin: "1rem",
width: 75,
height: 75,
borderRadius: "50%",
backgroundColor: color
}}
></div>
);
};
const MemoSwatch = memo(Swatch);
// 母層
const MemoExample = () => {
const [appRenderIdx, setAppRenderIdx] = useState(0);
const [clr, setClr] = useState("blue");
// 這裡新增一個更換按鈕的功能,來證實一下react memo的判斷機制
console.log(`Memo 渲染次數 ${appRenderIdx}`);
return (
<div>
<h4>React Memo 範例</h4>
<div className="f-b-c">
<button onClick={() => setAppRenderIdx(appRenderIdx + 1)}>
重新渲染
</button>
<button
onClick={() => (clr === "blue" ? setClr("red") : setClr("blue"))}
>
換顏色
</button>
</div>
<MemoSwatch color={clr} />
</div>
);
};
export default MemoExample;
這樣的使用是沒有問題的,但是當子層的色票組件(component)需要增加更多參數的時候,你會發現原本渲染的機制又壞掉了。如下:
import React, { memo, useCallback, useMemo, useState } from "react";
// 子層
// 更換了原本的結構從單一 color 字串改為物件 params 帶上 color key 去接值
// 這樣的作法更接近實際使用,藉由物件去擴充子層組件的彈性
const Swatch = ({ params }) => {
console.log(`Swatch 渲染 ${params.color}`);
return (
<div
style={{
margin: "1rem",
width: 75,
height: 75,
borderRadius: "50%",
backgroundColor: params.color
}}
></div>
);
};
const MemoSwatch = memo(Swatch);
// 母層
const MemoExample = () => {
const [appRenderIdx, setAppRenderIdx] = useState(0);
const [clr, setClr] = useState("blue");
console.log(`Memo 渲染次數 ${appRenderIdx}`);
return (
<div>
<h4>React Memo 範例</h4>
<div className="f-b-c">
<button onClick={() => setAppRenderIdx(appRenderIdx + 1)}>
重新渲染
</button>
<button
onClick={() => (clr === "blue" ? setClr("red") : setClr("blue"))}
>
換顏色
</button>
</div>
<MemoSwatch params={{ color: clr }} />
</div>
);
};
export default MemoExample;
這時你在觀看畫面的 console 會發現 swatch 又壞掉了,即便現在已經使用了 memo 來處理也是一樣,如下圖情況:
這是按下重新渲染按鈕之後的圖,如下:
這是按下換顏色按鈕的圖,如下:
為什麼會發生這種事情呢?
這就要回到前面 useEffect
的比較機制問題了,簡單來說在使用 React.memo 的同時,它在判斷你的組件是否需要更新是依據你傳入的 props,而傳入的 props 的比較機制又是採用同 useEffect
裏面 dependices array 的機制,也就是說當你傳入的是陣列(array)、物件(object)的話都要另外處理。
怎麼解決這種問題呢?下一篇再為大家揭曉答案,大家也可以想想怎麼處理。