3
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-06-14
[前回] Django+Reactで学ぶプログラミング基礎(24): Reactチュートリアル(ゲーム手番と勝者の判定)

はじめに

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

今回の内容

  • タイムトラベル機能を追加
    • 着手の履歴を保存
    • 再度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というstateGameコンポーネントに置き、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

  • 次に、BoardコンポーネントがGameコンポーネントから、squaresonClickプロパティを受け取るように

    • BoardonClickハンドラに、Squareの位置を渡す
      • どのマス目がクリックされたか伝える
  • 以下の手順でBoardコンポーネントを書き換える

    • Boardconstructorを削除
    • BoardrenderSquare
      • 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メソッドから不要となったコードを削除
    • リファクタリング後、Boardrender関数:
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の中身も修正が必要
    • GamehandleClickメソッドで、新しい履歴エントリを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

おわりに

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

[次回] Django+Reactで学ぶプログラミング基礎(26): Reactチュートリアル(着手履歴表示とKeyプロパティ)
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0