Django+Reactで学ぶプログラミング基礎(25): Reactチュートリアル(タイムトラベル機能)

##### [前回] [Django+Reactで学ぶプログラミング基礎(24): Reactチュートリアル(ゲーム手番と勝者の判定)](https://qiita.com/mingchun_zhao/items/d62d325ec27e8682dbd4)

## はじめに

前回は、三目並べゲームを完成させました。
今回は、追加のタイムトラベル機能です。
以前の着手へ`時間の巻き戻し`機能です。

## 今回の内容

- タイムトラベル機能を追加
    - 着手の履歴を保存
    - 再度`state`をリフトアップ

## 着手の履歴を保存

- タイムトラベルに必要な前提条件
    - ゲーム状態を保存する`squares`の配列をイミュータブルに扱う
        - 着手があるたびに`squares`のコピーを作り、元の配列を変更しない
    - `squares`の過去のバージョンをすべて保存することで
        - 過去の手番をさかのぼることができる

- 過去の`squares`の配列を、`history`という別の配列に保存
    - `history`配列は初手から最後まで、盤面の全ての状態を表現
    - 配列の構造:
```
history = [
  // 初期状態
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // 初手
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // 2手目
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]
```

## 再度`state`をリフトアップ

### 着手履歴`state`を、トップレベルの`Game`コンポーネントに表示

- `history`という`state`を`Game`コンポーネントに置き、`Game`コンポーネントがアクセスできるように
    - `Board`コンポーネントにある`state`を、トップレベルの`Game`コンポーネントにリフトアップ
    - これにより`Game`コンポーネントは`Board`のデータを完全制御できる
    - `history`内の過去の手番データを`Board`にレンダリングさせる
        - レンダリングとは、抽象的なデータから画像/映像/音声などを生成すること

### コードのリファクタリング

- まず、`Game`コンポーネントの初期`state`をコンストラクタ内にセット
```src/index.js
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
    };
  }

  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/ad904daf-1571-9142-8ef8-6c7fc031b82f.png)

- 次に、`Board`コンポーネントが`Game`コンポーネントから、`squares`と`onClick`プロパティを受け取るように
    - `Board`の`onClick`ハンドラに、`Square`の位置を渡す
        - どのマス目がクリックされたか伝える

- 以下の手順で`Board`コンポーネントを書き換える
    - `Board`の`constructor`を削除
    - `Board`の`renderSquare`で
        - `this.state.squares[i]`を`this.props.squares[i]`に置き換える
        - `this.handleClick(i)`を`this.props.onClick(i)`に置き換える
```src/index.js
class Board extends React.Component {
  handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

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

  render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    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>
    );
  }
}
```

- `Game`コンポーネントの`render`関数を更新
    - ゲームのステータステキストの決定や表示の際、最新履歴が使われるように
```src/index.js
  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
```

- `Board`内の`render`メソッドから不要となったコードを削除
    - リファクタリング後、`Board`の`render`関数:
```src/index.js
  render() {
    return (
      <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>
    );
  }
```

- 最後に、`handleClick`メソッドを`Board`から`Game`に移動
    - `Game`コンポーネントの`state`構成が変わったため
        - `handleClick`の中身も修正が必要
    - `Game`の`handleClick`メソッドで、新しい履歴エントリを`history`に追加
        - ※ エントリ追加に、`push()`メソッドでなく、`concat()`を使用する理由
            - 元の配列をミューテート(書き換え)しないため
```src/index.js
  handleClick(i) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares,
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }
```

### 最終的に、各コンポーネントの構成

- `Game`コンポーネント
    - ゲームの`state`
    - `handleClick`メソッド
- `Board`コンポーネント
    - `renderSquare`メソッド
    - `render`メソッド

- アプリをデバッグ起動し(F5)、ブラウザで確認
    - コードをリファクタリングしただけなので、表示は変わりません
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/6c30c8ce-ca52-2cec-71d6-af4d8f9496ac.png)

## おわりに

過去の着手履歴を保存するため、コードをリファクタリングしました。
着手履歴の画面表示は、次回に持ち越しとなります。
お楽しみに。

##### [次回] [Django+Reactで学ぶプログラミング基礎(26): Reactチュートリアル(着手履歴表示とKeyプロパティ)](https://qiita.com/mingchun_zhao/items/55696ea89f9d05eccc13)