iT邦幫忙

2023 iThome 鐵人賽

DAY 8
0
Modern Web

React 走出新手村 系列 第 8

React 走出新手村-深入Memo

  • 分享至 

  • xImage
  •  

這次來聊聊進階一點的話題 memo 的應用,在談論這個話題之前我們先來了解 memo 是什麼?
https://ithelp.ithome.com.tw/upload/images/20230904/20129020STl1iyT9GJ.jpg

了解 React.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

此時你的畫面應該會看到如下圖:
https://ithelp.ithome.com.tw/upload/images/20230904/20129020ItxhvM0yja.png

當我們按下按鈕時會發現子層的色塊圓圈也跟著重新渲染了,即使他的 props 根本沒發生變化,如下圖:
https://ithelp.ithome.com.tw/upload/images/20230904/201290201OnDkuEuYm.png

想要解決以上的問題可以很簡單的利用 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;

我留了兩個色塊讓大家可以比較容易看到差異,那畫面應該會如下圖:
https://ithelp.ithome.com.tw/upload/images/20230904/20129020Pa43y944rW.png

當按下按鈕時會看到有用React.memo處理的component已經不會因為重新渲染按鈕動作而觸發無意義的重複渲染了,如下圖:
https://ithelp.ithome.com.tw/upload/images/20230904/20129020RbJXDw2hiS.png

這裡稍微提一下新手常常會弄錯的地方,包含我自己在剛學習使用的時候也時常會搞混,要實現這個範例前我們先加一個更換顏色的功能上去,如下:

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 來處理也是一樣,如下圖情況:
https://ithelp.ithome.com.tw/upload/images/20230904/20129020tGTHOATwXb.png

這是按下重新渲染按鈕之後的圖,如下:
https://ithelp.ithome.com.tw/upload/images/20230904/20129020rIWDQ9BeQx.png

這是按下換顏色按鈕的圖,如下:
https://ithelp.ithome.com.tw/upload/images/20230904/201290207Ai2IvL3B5.png

為什麼會發生這種事情呢?

這就要回到前面 useEffect 的比較機制問題了,簡單來說在使用 React.memo 的同時,它在判斷你的組件是否需要更新是依據你傳入的 props,而傳入的 props 的比較機制又是採用同 useEffect 裏面 dependices array 的機制,也就是說當你傳入的是陣列(array)、物件(object)的話都要另外處理。

思考時間

怎麼解決這種問題呢?下一篇再為大家揭曉答案,大家也可以想想怎麼處理。

給全新手的大禮包

React基本Hook教學


上一篇
React 走出新手村-深入useRef
下一篇
React 走出新手村-Memo處方簽
系列文
React 走出新手村 31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言