JavaScript

[React初心者向け] Component と PureComponentの違いを理解する

React

今日は、reactのパフォーマンス改善時に使用されるテクニックの一つであるPureComponentを紹介します。PureComponentとComponentの違いをマスターし、shouldComponentUpdateが何をするのかを理解して、パフォーマンスを改善しましょう!

Contents

PureComponent vs Component 何が違うのか?

まずはReactの公式ドキュメントを確認してみましょう。

React.PureComponent はReact.Componentと似ています。両者の違いは React.Component shouldComponentUpdate() を実装していないことに対し、React.PureComponent  props state を浅く (shallow) 比較することでそれを実装していることです。

出典:https://ja.reactjs.org/docs/react-api.html#reactpurecomponent

まとめると、

React.PureComponent は、 shouldComponentUpdate() を実装しており、React.Componentは、 shouldComponentUpdate() を実装していない。これだけです。

つまり、shouldComponentUpdate()が何をするのか?を理解することが、PureComponent とComponentの違いを理解するための鍵となります

shouldComponentUpdate()とは何か?

shallow compareをするファンクションです。比較対象(stateやpropsとか)のものが、等しいかどうか浅く確認します。

浅く???聞いたことない。。。という初心者向けに、JavaScriptのリファレンス(参照)の話を交えて、もう少し詳しく解説します。

比較対象がスカラー(整数、論理値、文字列など)の時

こっちは簡単です。その値(value)自体を比較しますので、プログラミング初心者でもすんなり理解できるでしょう。Reactのstateやpropsはオブジェクトですので(例えば、this.state = { isShow: true }という感じ)、こちらのケースは直接は関係ありません。 

単純にスカラーの整数を比較するとは、だったものが、に変化していないかなど。

比較対象(stateやprops)が複合型(配列、オブジェクトなど)の時

注意すべきはこちらのケースです。Reactのstateやpropsは、オブジェクトですので、今回のpureComponentに関わってくるのもこちらです。

結論から言えば、オブジェクトの場合のshallow compareは、レファレンス(参照)のみチェックし、オブジェクトの中の値(value)はチェックしません

オブジェクトの例を下記で説明します。

const old = { name: "wata", age: 100};
const young = old; //ここがよろしくない! oldのreferenceが入る。
young.age = 20;
console.log(young); // {name: "wata", age: 20}
console.log(old); // {name: "wata", age: 20} うん??? 100だったような、、、
console.log(old === young); // true

ですので、oldとyoungを比較した時に、trueが返りました。この例では、オブジェクトの中身の値が同じですが、もし異なっていたとしても、レファレンス(参照)が同じであれば、常にtrueが返り、同じものであると見なされます。そうなれば、shouldComponentUpdate()はアップデートする必要はないと判断し、falseを返し、そのコンポーネントはリレンダーされませんので注意してください。

上記のようにデータを操作することをmutateと言います。例のように、意図していないバグが発生しやすいので、mutateは避け、例えば、下記のような方法で、データを更新しましょう.

const old = { name: "wata", age: 100};
const cloned = Object.assign({}, old);
console.log(cloned); // {name: "wata", age: 100}
console.log(old === cloned); // false えっ???

そうなんです。こちらは、オブジェクトの中の値は完全に一致しているのに、falseが返りました。理由は先ほどと同じで、オブジェクトの中の値を比較しているのではなく、レファレンスをチェックしているからです。clonedというオブジェクトは、Object.assign()によって作られた、全く新しいオブジェクトです。よって、oldとclonedという別のオブジェクトを比較しているので(リファレンスが異なる)、falseが返ります。そうなれば、propsに変更があったと判断されshouldComponentUpdate()はtrueを返し、そのコンポーネントはリレンダーされます。

と、ここまでJavaScriptのリファレンスの話を交えて説明してきましたが、これがわかってないと、React.pureComponentの使用で、逆にバグを作ってしまうかもしれませんので、注意しましょう。

PureComponent の使いどころは?

基本的には通常のReact.Componentを利用し、パフォーマンスを最適化するときの一つの手段として、PureComponentを利用を考慮するべきです。

React自体VirtualDOMで差分しかrenderされず性能を保証してくれるため、初心者は気にせずComponentを使用すべきでしょう。パフォーマンスに問題が生じた時に初めてPureComponentを利用を考慮すれば良いでしょう。

つまり、PureComponentは、どこでも使えばいいと言うものではありません。これに関し、FacebookのReact、Reduxの開発者のDan Abramov氏も

どこでも使えばいいのなら、PureComponentがデフォルトになっているはずだけど、実際にはなっていない。

いつもコンポーネントをリレンダーするかどうかのチェックをしていたら、その処理はおそらく無駄でしょう。

と、以前Twitterでコメントしていました。

それでは、PureComponentを使うべきケースとそうでないケースにわけて、以下、まとめます。

PureComponentを使うべきケース

  1. コンポーネントのpropsやstateが変化しない時。変化しないのに、リレンダーしても意味ないので。よくある具体例を紹介すると、親コンポーネントがアップデートされて、リレンダーされる時に、子のコンポーネントのpropsstateが変わらないケースです。この時、子のコンポーネントに対してPureComponentを使用することで、無駄なリレンダーを回避でき、パフォーマンス向上を見込めます(後ほど実装例を紹介します)。少し考えてみると当たり前ですが、ルートに近いコンポーネントにPureComponentを実装するほど、パフォーマンスの向上は大きくなります。逆に言えば、最下層のコンポーネントに実装してもリレンダーを回避できるのは、そのコンポーネントのみとなるので、パフォーマンス向上は小さくなります。
  2. コンポーネントのpropsやstateがimmutableである時。言い換えると、propsやstateをパスするときには、つねに新しく生成したpropsやstateを渡しましょう、ということです。

PureComponentが使うべきでないケース

  1. propsstateが常に変化する場合。通常のComponentを使うべきです。shouldComponentUpdate内のshallowEqualにも処理に時間がかかるので、常に変化するのなら、わざわざ余分に比較する必要はありません。
  2. propsやstateがmutableである時。上記の「比較対象(stateやprops)が複合型(配列、オブジェクトなど)の時」で説明の通りですが、propsやstateがmutableである時には、shouldComponentUpdate()は常にfalseを返し、そのコンポーネントはリレンダーされませんので注意してください。

PureComponent実装時の注意点

上記での「比較対象(stateやprops)が複合型(配列、オブジェクトなど)の時」で説明した通りですが、React.PureComponentのshouldComponentUpdate() は、オブジェクトを浅く比較するのみです。つまり言い換えれば、ネストされた階層の深いオブジェクトで、propsが変更されても、そのコンポーネントはリレンダー(更新)されないと言う問題が生じる可能性がありますので、注意してください。

PureComponent の実装例

それでは、シンプルなメッセージリストの実装でpureComponentの動きを確認します。ちなみに、codesandboxというサイトでdemoを作成しています 。

import React, { Component, PureComponent } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

// ルートコンポーネント
// input formにメッセージを入力するごとに、それをリスト形式で表示するだけのコンポーネント
export default class App extends Component {
  constructor() {
    super();
    this.state = { messages: [] };
    // binding
    this.getLastMessage = this.getLastMessage.bind(this);
    this.onChangeMessage = this.onChangeMessage.bind(this);
  }

  getLastMessage() {
    const lastMessage = this.state.messages[this.state.messages.length - 1];
    return lastMessage === undefined ? "" : lastMessage;
  }

  onChangeMessage(e) {
    const messages = [...this.state.messages];
    messages.push(e.target.value);
    this.setState({ messages: messages });
  }

  render() {
    return (
    <div className="App">
        <input
          type="text"
          value={this.getLastMessage()}
          onChange={this.onChangeMessage}
          style={{ margin: "10px" }}
        />
        <MessageList messages={this.state.messages} />
      </div>
    );
  }
}

// メッセージのリストを表示するだけのコンポーネント
class MessageList extends Component {
  render() {
    return (
      <ul>
        {this.props.messages.map((m, i) => (
          <Message key={i} message={m} />
        ))}
      </ul>
    );
  }
}

// 一つのメッセージのみを表示するだけのコンポーネント
class Message extends Component {
  render() {
    console.log("Render;", this.props.message);
    return <li style={{ margin: "10px" }}> {this.props.message} </li>;
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
入力するごとに全てのメッセージがリレンダーされている入力するごとに全てのメッセージがリレンダーされている

consoleのログを見ると、インプットに入力するごとに全てのmessageがリレンダーされているのが確認できます。これは、明らかに無駄なアップデートです。ここで、pureComponentの出番です。

// MessageコンポーネントをpureComponentにする
class Message extends PureComponent {
 render() {
  console.log("Render;", this.props.message);
  return <li style={{ margin: "10px" }}> {this.props.message} </li>;
}}
pureComponentにより無駄なリレンダーを防ぐpureComponentにより無駄なリレンダーを防ぐ

consoleのログがスッキリし、無駄なリレンダーがなくなったことを確認できます。

参考としたサイト

https://ja.reactjs.org/docs/reconciliation.html

ABOUT ME
Wata
30歳で営業職からエンジニアに転職した者のブログです。 大手海運業→総合商社→ソフトウエアエンジン。現在は、オーストラリアにてエンジニアとして働いています。 未経験からのエンジニアへの転職、フロントエンド周りの技術、エンジニアの仕事環境、趣味の旅行、JAL修行、オーストラリアの情報などを発信しています!

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です