從零開始的 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.memo 與 PureComponent 一樣都使用 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
Higher-Order Components – React