Reactにおけるstateのイミュータビリティ


この記事の目的

公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。

関連するQiita記事

Reactのざっくり概要
Reactコンポーネントとは
Reactコンポーネント間の値の受け渡し
Reactコンポーネントでstateをリフトアップ

参考資料

ドキュメント

公式ドキュメント

チュートリアル

公式チュートリアル

※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough Tutorial

Reactにおけるstateのイミュータビリティ

イミュータビリティとは

変更可能なデータオブジェクトの値を変更するときは、以下の2つの方法がある。

  1. 対象のデータオブジェクトを直接変更する → mutable(変異)
  2. 対象のデータオブジェクトをコピーし、コピーしたデータオブジェクトを変更してから対象のデータオブジェクトをコピーオブジェクトで入れ替える(書き換えではなく入れ替え) → immutable (不変)

Reactにおけるイミュータビリティ

Reactのstateを変更する際はsetState()という関数を使用するが、この関数はイミュータブルの手法で値を変更する必要がある。

コードの例

以下のBoardコンポーネントを例に説明する。

board.js
class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();  //ポイント1
    squares[i] = 'X';  //ポイント2
    this.setState({squares: squares});  //ポイント3
  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

BoardコンポーネントのhandleClick関数(Squareコンポーネントでマスをクリックされたときに動作する関数)においてsetState関数を使用してstateの置き換えをしているが、その中でイミュータブルな処理が行われている。

注目すべきポイント

見るべきポイントは以下の3点。

  1. state.squaresの値を変更したいが、まずはconst squares = this.state.squares.slice();state.squaresのコピーを作成する
  2. コピーしたsquaresに'X'を格納する
  3. this.setState({squares: squares});で、state.squaresをコピーしたsquaresで置き換える → 書き換えではなく置き換えをしている

オブジェクトをコピーする方法

JavaScriptでオブジェクトのコピーを作成する方法はいろいろあるが、「配列をコピーする例」と「オブジェクトのプロパティをコピーする例」を示す。

配列をコピーする例

Array.prototype.slice()

ArrayCopy.js
const newSquares = this.state.squares.slice();
newSquares[i] = 'X';
this.setState({squares: squares});

オブジェクトのプロパティをコピーする例

Object.assign()

ObjectAssign.js
var player = {score: 1, name: 'Jeff'};
var newPlayer = Object.assign({}, player, {score: 2});
this.setState({player: newPlayer});

なぜイミュータブルであることが重要なのか

なぜ、書き換えによるmutable(変異)な値の変更を行わず、オブジェクトの入れ替えによってimmutable (不変)な値の変更を行うのが重要なのか、理由を3つ説明する。

複雑な処理を簡略化できる

stateをimmutable (不変)の方針でオブジェクトによって置き換えると、値の変更は「オブジェクトの置き換えをしていない」か「オブジェクトの置き換えをしている」かの2択になる。
ある一部分だけ値を書き換えたような中途半端な状態にはならない。

対して、stateをmutable(変異)の方針で直接書き換えると、ある一部分だけ書き換わった中途半端な状態が存在する可能性がある。
仮に、途中で書き換えの処理が失敗した場合、「一部分だけ書き換わった中途半端な状態」を元に戻す複雑な処理が必要となってしまう。

また、immutableで「オブジェクトの置き換えをした結果」を履歴として保持すれば、過去の状態に戻す処理もmutableよりも簡単に実現することができる。

変更の検出が容易

mutableで値を直接書き換えた場合、stateが参照するオブジェクトに変化がないため、state内のどのオブジェクトに変化があったのかをReactは検知することができない。
そうなると、Reactは仮想DOM内の新旧差分を全走査することになり、再描画が遅くなる。

対して、imutableでstateのオブジェクトを入れ替えた場合、stateが参照するオブジェクトに変化があるため、state内のどのオブジェクトに変化があったのかをReactは検知できる。
ReactはViewの再描画をする際に、仮想DOMの新旧比較を変化があったオブジェクトでのみ行うため、再描画が高速になる。

再描画のタイミングを決めやすい

上記で書いたとおり、imutableなオブジェクトはstate内のどこで変更があったのかを検知できる。
それを利用して、再描画が必要ない変更であれば再描画せず、再描画が必要な変更であれば差描画するといったことが可能となる。
それによって、不要な再描画を押さえて描画パフォーマンスを向上させることができる。