iT邦幫忙

2022 iThome 鐵人賽

DAY 16
0

說明

React.memo 是用來減少不必要被更新的元件被重新渲染。當父元件的資料狀態被更新時,若子元件相關的 props 沒有因此被更新時,這個子元件就不需被重新渲染。React 提供了 React.memo 來達成這個功能。

React.memo是 一個 Higher Order Component

語法

const MemoMyComponent = React.memo(MyComponent)

or 

export default React.memo(MyComponent)

透過以上語法,將子元件包在 React.memo 之中,React 發現 props 相同時,會忽略 Render 這個子元件,並直接重用上次的 Render 結果

這預設只會對 props 進行 shallow compare。(後面會介紹)

如果你需要控制比較的方法,你可以提供一個自訂比較的 Function 作為第二個參數。

const MyComponent = (props) => {
  /* render using props */
}
const areEqual = (prevProps, nextProps) => {
  // 自訂比較的 Function
}
export default React.memo(MyComponent, areEqual);

React.memo 共接收兩個參數,第一個是要包住的元件,第二個是可以自訂比較 props 的方法,回傳 false 時會重新渲染元件。

注意 props 的 shallow compare

元件 props 的比對,在原生型態 (string/number/boolean) 是 call-by-value,但是在物件型態 (object/array/function) 還是 call-by-reference,所以當 props 是物件型態時,比較的是記憶體位置。

當父元件重新渲染時,傳入 props 給子元件,如果是物件型別的話,即使傳入 props 的值完全一樣,因為記憶體位置不同,還是會導致 React.memo 失效,重新渲染子元件。

解決辦法

  1. 使用 memo 的第二個參數,自訂比較 props 的方法。
  2. 使用 useCallback,因此 memo 與 useCallback 經常被一起使用。(通常是傳入子元件的事件的 HandlerFunction 沒有被 Memorized,而導致重新渲染。)

範例

使用 memo 的第二個參數,自訂比較 props 的方法

定義三個子元件如下

  • Child 元件,沒有使用 React.memo 加工
  • ChildMemo 元件,使用 React.memo 加工,但沒有傳入自訂比較 props 的方法
  • ChildMemoCompare 元件,使用 React.memo 加工,且傳入自訂比較 props 的方法 - compareFn

這裡傳入二個 props,一個是 Status,一個是 Obj,在沒有使用 React.memo 時,改變 props 會觸發 Re-Render。

const Child = (props) => {
  console.log(`Child render`);
  return <p>Child number is : {props.obj.number}</p>;
};

const ChildMemo = React.memo((props) => {
  console.log(`ChildMemo render`);
  return <p>ChildMemo number is : {props.obj.number}</p>;
});

const compareFn = (prevProps, nextProps) => {
  if (prevProps.obj.number !== nextProps.obj.number) {
    return false;
  }
  return true;
};

const ChildMemoCompare = React.memo((props) => {
  console.log(`ChildMemoCompare render`);
  return <p>ChildMemoCompare number is : {props.obj.number}</p>;
}, compareFn);

const App = () => {
  console.log('App render');
  const [status, setStatus] = React.useState(false);
  const [obj, setObj] = React.useState({number: 1});
  
  const handleChangeObj = () => {
    setObj({number: obj.number + 1});
  };

  return (
    <>
      <button onClick={() => setStatus(!status)}>Change Status</button>&nbsp;
      <button onClick={handleChangeObj}>Change Object</button>
      <Child status={status} obj={obj} />
      <ChildMemo status={status} obj={obj} />
      <ChildMemoCompare status={status} obj={obj} />
    </>
  );
}

觀察 ChildMemo 可以發現當 props 傳入的是物件時,即使傳入 props 的值完全一樣,因為記憶體位置不同,還是會導致 React.memo 失效,在這個範例會發現 ChildMemo 還是被重新渲染。

而 ChildMemoCompare 會再去使用 compareFn 做比對,所以當只有改變 Obj.number 的值時才會觸發 ChildMemoCompare Re-Render,改變 Status 時則不會 Re-Render。

執行結果:https://codepen.io/lala-lee-jobs/pen/bGMWaEx?editors=0011

使用 useCallback 搭配 memo

定義二個子元件如下

  • ChildMemo 元件,使用 React.memo 加工,但 props 傳入的 reset Function 不使用 useCallback (resetCount)
  • ChildMemoCallback 元件,使用 React.memo 加工,但 props 傳入的 reset Function 有使用 useCallback (resetCountUseCallback)
const ChildMemo = React.memo((props) => {
  console.log("ChildMemo Render");
  return (
    <div>
      <span>ChildMemo</span>&nbsp;
      <button onClick={props.reset}>ChildMemo Reset Count</button>
    </div>
  );
});
const ChildMemoCallback = React.memo((props) => {
  console.log("ChildMemoCallback Render");
  return (
    <div>
      <span>ChildMemoCallback</span>&nbsp;
      <button onClick={props.reset}>ChildMemoCallback Reset Count</button>
    </div>
  );
});

const App = () => {
  console.log("App render");
  const [count, setCount] = React.useState(0);
  const resetCount = () => {setCount(0)};
  const resetCountUseCallback = React.useCallback(() => {
    setCount(0);
  }, []);
  return (
    <>
      <div>
        <span>App Count: {count}</span>&nbsp;
        <button onClick={() => setCount((count) => count + 1)}>Increment</button>
      </div>
      <ChildMemo reset={resetCount} />
      <ChildMemoCallback reset={resetCountUseCallback} />
    </>
  );
}

觀察 ChildMemo,當按下 Increment 時,雖然 ChildMemo 畫面不需變動,還是被 Re-Render;按下 ChildMemo 元件上的 Reset,ChildMemo 畫面不需變動,還是被 Re-Render。

觀察 ChildMemoCallback,當按下 Increment 時,ChildMemoCallback 畫面不需變動,不會被 Re-Render;按下 ChildMemoCallback 元件上的 Reset,ChildMemoCallback 畫面不需變動,不會被 Re-Render。

執行結果:https://codepen.io/lala-lee-jobs/pen/qBYmpgP?editors=0011

加上優化功能不一定就是好

在使用 useMemo、useCallback及 memo 做優化時,要注意一些使用情境,因為處理優化也是會佔用效能,可能讓畫面 Re-Render 的效能都比處理優化的效能來得好。

  • memo - 當經常傳入不一樣的 props 不建議使用 memo,因為要耗費比對及記憶的效能,所以要評估元件的大小,來決定是否值得使用。
  • useMemo - 主要用在當元件重新渲染時,減少在元件中複雜的程式重複執行,否則一般不需要特別使用。
  • memo 與 useCallback 是經常被用來作為組合技的方法,memo 能夠偵測 props 有沒有修改,減少元件不必要的渲染;useCallback 讓 props 的 Function 能被記住不會改變記憶體位置。雖然 memo 傳入的 Function 是物件,但因為記憶體位置不變,所以不會被重新渲染。

Next

一旦專案越來越複雜,不同的操作方式會變更不同的狀態,要實作出完善的功能,狀態管理就變得十分重要,接下來幾篇文章,會開始介紹 React 與狀態管理相關的各種使用情境。

Reference

https://www.w3schools.com/react/react_memo.asp

https://ithelp.ithome.com.tw/articles/10268792

https://medium.com/%E6%89%8B%E5%AF%AB%E7%AD%86%E8%A8%98/react-optimize-performance-using-memo-usecallback-usememo-a76b6b272df3


上一篇
Day 15 Memorized Hook - useMemo、useCallback
下一篇
Day 17 初探狀態管理 - Flux 架構 與 useReducer
系列文
開始搞懂React生態系30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言