從零開始的 React 教學 Part 6 - React.memo

Posted on  Oct 13, 2022  in  ReactJS 前端框架  by  Amo Chen  ‐ 3 min read

從零開始的 React 教學 Part 5 - PureComponent 中提到 PureComponent 透過 shallow comparison 加速比較 props / state 的速度,為 React 應用(application)帶來更好的效能。

p.s. 雖然 PureComponent 提升比較速度,但也犧牲比較時的精準度,使用時一定要理解何謂 shallow comparison, 才不會帶來預期外的 bug

不過 PureComponent 需要以繼承 PureComponent 類別(class) 的方式實作 Class Component ,對於某些 Functional Components 來說,沒辦法這樣進行。

所幸, React 也有提供 React.memo API ,讓開發者能夠將 Functional Component 用 React.memo 包裝起來,達到類似 PureComponent 的作用。

本文將介紹 React.memo API 如何使用,以及使用上應注意的點。

本文環境

  • React

React.memo

If your component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.

React.memo 很適合用在只要給定相同 props ,就會產生一致結果的元件,例如下列範例中的 <Title> 元件,每次 <App> 元件因為更新 count 重新渲染時,就也會跟著重新渲染一次,即使它的 props.value 的值都沒改變:

import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';

function Title(props) {
  console.log('render title');
	return (
		<h1>{props.value}</h1>
	);
}

class App extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
        count: 0,
      }
  }

  addCount = () => {
      let count = this.state.count + 1
      this.setState({count: count})
  }

  render() {
    return (
      <div>
        <Title value="Click the following line to update count" />
        <div onClick={this.addCount}>Count: {this.state.count}</div>
      </div>
    );
  }
}

const domContainer = document.querySelector('#app');
const root = ReactDOM.createRoot(domContainer);
root.render(<App />);

上述範例的執行結果,如下圖所示,從結果可以發現更新 count 時, <Title> 元件也跟著又渲染一次:

這種情況下,每次都重新渲染 <Title> 元件的話,就會造成效能問題,因為一直重複在做只需要做一次的事。

所以可以針對這種情況,用 React.memo<Title> 元件包裝起來,減少重複渲染的情況:

import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';

function Title(props) {
  console.log('render title');
	return (
		<h1>{props.value}</h1>
	);
}

const MemoTitle = React.memo(Title);

class App extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
        count: 0,
      }
  }

  addCount = () => {
      let count = this.state.count + 1
      this.setState({count: count})
  }

  render() {
    return (
      <div>
        <MemoTitle value="Click the following line to update count" />
        <div onClick={this.addCount}>Count: {this.state.count}</div>
      </div>
    );
  }
}

const domContainer = document.querySelector('#app');
const root = ReactDOM.createRoot(domContainer);
root.render(<App />);

上述範例的執行結果,如下圖所示,從結果可以發現更新 count 時, <Title> 元件不會跟著重新渲染了:

這就是 React.memo 的作用——減少不必要的重新渲染以改善效能

使用 React.memo 應注意的事 - shallow comparison

React.memoPureComponent 一樣都使用 shallow comparison 進行比較, PureComponent 比較的是 props 與 state, 而 React.memo 僅針對 props 進行比較。

因此使用 React.memo 時也必須 shallow comparison 所帶來的問題,像是 object, array 這類資料型態, shallow comparison 比較的是在記憶體中的參照(reference),所以有可能造成 object 或 array 內的值已經改變了,但是元件卻沒有重新渲染的情況,這個情況可以參考以下範例

import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';

function Title(props) {
  console.log('render title');
	return (
		<h1>{props.value.text}</h1>
	);
}

const MemoTitle = React.memo(Title);

class App extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
        count: 0,
      }
      this.value = {
        text: 'Click the following line to update count'
      }
  }

  addCount = () => {
      let count = this.state.count + 1
      this.setState({count: count})
      this.value.text = 'The text changed but you can not see'
  }

  render() {
    return (
      <div>
        <MemoTitle value={this.value} />
        <div onClick={this.addCount}>Count: {this.state.count}</div>
      </div>
    );
  }
}

const domContainer = document.querySelector('#app');
const root = ReactDOM.createRoot(domContainer);
root.render(<App />);

關於更詳細的 shallow comparison 所造成的問題,可以參閱 從零開始的 React 教學 Part 5 - PureComponent 一文。

如果對於元件何時必須更新的條件非常清楚的話,可以在呼叫 React.memo 時,代入第 2 個參數,該參數必須是 1 個函數,用以比較變動前後的 props, 該函式回傳 false, 則代表需要重新渲染,若回傳 true, 則代表可以不用重新渲染。

下列範例就是實作函式 areEqual 並且在呼叫 React.memo 時,作為第 2 個參數代入,該函式會負責比較 props 變動前後的值,讓 React 明確知道是否該重新渲染該元件,也從而避免 shallow comparsion 所造成的問題:

import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';

function Title(props) {
  console.log('render title');
	return (
		<h1>{props.value.text}</h1>
	);
}

function areEqual(prevProps, nextProps) {
  return prevProps.value.text !== nextProps.value.text
}

const MemoTitle = React.memo(Title, areEqual);

class App extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
        count: 0,
      }
      this.value = {
        text: 'Click the following line to update count'
      }
  }

  addCount = () => {
      let count = this.state.count + 1
      this.setState({count: count})
      this.value.text = 'The text changed'
  }

  render() {
    return (
      <div>
        <MemoTitle value={this.value} />
        <div onClick={this.addCount}>Count: {this.state.count}</div>
      </div>
    );
  }
}

const domContainer = document.querySelector('#app');
const root = ReactDOM.createRoot(domContainer);
root.render(<App />);

HOC(Higher-Order Component)

順帶一提,React.memo 是 1 種稱為 HOC 的元件,具體來說,HOC 是 1 種進階的技巧,是 1 個可以接受元件做為參數,並回傳新元件的函式,如果運用得當,可以提高複用元件邏輯的程度。

Concretely, a higher-order component is a function that takes a component and returns a new component.

不過 HOC 的實作上還有些原則需要遵守,礙於篇幅限制,對 HOC 有興趣的話,可以詳閱此文 Higher-Order Components – React 提高 React 開發技巧。

References

React Top-Level API – React

Higher-Order Components – React

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!