LoginSignup
1
3

More than 3 years have passed since last update.

Reactチュートリアルを噛み砕く

Posted at

できる限り丁寧にReactチュートリアルの内容をまとめました。
なお、こちらの記事は完全なるHYOP(独りよがりアウトプット)であるということを、はじめにご承知いただきたく存じます。

*本記事ではcloud9を用いて開発を進めていきます*
※一部、オリジナルの内容を変更しています。

初期設定

まずはnodeの確認

ec2-user:~/environment(master)$ node -v
v14.2.0

react-boardgameという名前でプロジェクトを作成します。

ec2-user:~/environment(master)$ npx create-react-app react-boardgame

作成したプロジェクト内の、srcディレクトリにあるソースファイルを削除します。

ec2-user:~/environment (master) $ cd react-boardgame/
ec2-user:~/environment/react-boardgame (master) $ cd src
ec2-user:~/environment/react-boardgame/src (master) $ rm -f *
ec2-user:~/environment/react-boardgame/src (master) $ cd ..
ec2-user:~/environment/react-boardgame (master) $ 

srcディレクトリ配下に、新たに index.css、index.js を追加します。

ec2-user:~/environment/react-boardgame (master) $ touch src/index.css src/index.js

それぞれのファイルに記述するコードを下に記します。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {/* TODO */}
      </button>
    );
  }
}

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }

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

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);
index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

下記のコマンドを実行し、サーバーを立ち上げます。

ec2-user:~/environment/react-boardgame (master)$ npm start

エディタ上部にあるPreviewタブからPreviewRunningApplicationを選択し、
スクリーンショット 2020-05-21 13.45.06.png
このような画面が表示されればOK。

ところで、Reactとは何か。

React はユーザインターフェイスを構築するための、宣言型で効率的で柔軟な JavaScript ライブラリです。複雑な UI を、「コンポーネント」と呼ばれる小さく独立した部品から組み立てることができます。 
(公式ドキュメントから引用)

とのことです。

class コンポーネント名 extends React.Component

から始まる部分がコンポーネントに当たります。
今回のコードで言うとSquare Board Gameの三つです。

データの流れとしては
【親】Game〜>Board〜>Square【子】
となり、この過程で、Propsというパラメーターが親コンポーネントから子コンポーネントに受け渡されています。

抽象的な例えかもしれませんが、
Squareコンポーネントは、二次元的です。「一つ一つのマスにどんな値を示すか」にフォーカスしています。
Boardコンポーネントは、三次元的です。奥行きを持って盤面の全体を俯瞰し、Squareに対して表示するべきデータを渡します。
Gameコンポーネントは、そこに時間軸が加わった四次元的な存在です。ゲーム全体の情報を記憶し、それぞれの着手に対応した盤面の情報をBoardに渡します。

文章だけではアレなので、実際に動かしていきましょう。

データを Props 経由で渡す

BoardコンポーネントのrenderSquareメソッド内で、Squareコンポーネントにvalueという名前のpropsを渡します。

index.js

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />;
  }
}

そして、Squareコンポーネントのrenderメソッド内で、渡された値を受け取るため{/* TODO */}を{this.props.value} に書き換えます。

index.js

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}
      </button>
    );
  }
}

スクリーンショット 2020-05-21 15.17.12.png
数字が表示されればOKです。

インタラクティブなコンポーネントを作る

Squareコンポーネントをクリックした際に、'X'が表示されるように書き換えていきます。
それに際して、Squareコンポーネントに自分がクリックされたことを "覚えさせる" 必要がありますね。
コンポーネントが何かを「覚える」ためには、state というものを使います。

index.js

class Square extends React.Component {

 constructor(props){
      super(props);
      this.state={
         value: null,                               
      };
  }

  render() {
    return (
      <button 
          className="square" 
         onClick={() => this.setState({value: 'X'})} 
      >
       {this.state.value}                            
      </button>
    );
  }
}

変更点は3点
1. コンストラクタの追加
2. renderメソッドのonClickプロパティ
3. {this.props.value} => {this.state.value} の書き換え
です

1.のステップを経て、コンストラクタ内でthis.stateに初期値を定義します。
(参考 コンストラクタとは:https://wa3.i-3-i.info/word13646.html)

2.のステップで、Squareコンポーネントがクリックされた際に、this.stateの値が'X'に置き換わるように変更を加えます。

3.のステップで、マスの中にthis.stateの値が表示されるように設定しています。

スクリーンショット 2020-05-21 16.29.11.png
マスをクリックして値が変更されればOKです。

また、constructorメソッドの中でsuper(props)を指定しますが、
なぜこれを書くのかはこちらの記事を参考にしてみてください。(翻訳してくださりありがとうございます。)

State のリフトアップ

ゲームの基本的な部品が揃いました。完全に動作するゲームにするためには、盤面に “X” と “O” を交互に置けるようにすることと、どちらのプレーヤが勝利したか判定できるようにする必要があります。

現状、各々のsquareが「null」であるべきか「X」であるべきかを、自身で判断しています。しかしこれでは勝敗はわかりません。
勝敗を判断するためには、盤面全体の状態を認識する鳥の目、つまり、Boardコンポーネントが必要になります。

方法として、BoardからSquareに対して現状の値を問い合わせるという方法もある(らしい)のですが、コードが壊れやすくわかりづらいものになってしまうのです。
なのでここでは、Boadsがマスの状態を管理し、それをpropsで各Squareに渡すことで、ゲームの管理をすることとします。

複数の子要素からデータを集めたい、または 2 つの子コンポーネントに互いにやりとりさせたいと思った場合は、代わりに親コンポーネント内で共有の state を宣言する必要があります。親コンポーネントは props を使うことで子に情報を返すことができます。こうすることで、子コンポーネントが兄弟同士、あるいは親との間で常に同期されるようになります。 
(公式ドキュメントから引用)

まずはBoardコンポーネントに変更を加えましょう。

index.js


class Board extends React.Component {

  ⬇︎ステップコンストラクタを追加
  constructor(props){
      super(props);
      this.state = {
          squares: Array(9).fill(null),
      };
  }

  ⬇︎ステップhandleClickメソッドを追加
  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

  ⬇︎ステップpropsの内容を変更
  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>
    );
  }
}

順番に見ていきましょう。まずはコンストラクタについてです。
this.stateの中身に注目してください、

index.js ステップ①
  constructor(props){
      super(props);
      this.state = {
          squares: Array(9).fill(null),⬅︎ココ
      };
  }

squaresという名の、9つのnull要素を持つ配列を生成しています。
Squaresにあったコンストラクタとやっていることは一緒ですね。

続いて、renderSquareメソッドの変更を確認していきます。

index.js ステップ②
renderSquare(i) {
    return (
      <Square 
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

ここで指定しているのは、Squareコンポーネントに実際に渡すpropsの値でしたね。
valueには、引数をインデックス番号としてsquaresから取り出した値を、
onClickには、BoardコンポーネントのhandleClickメソッドを指定しています。

そのhandleClickメソッドがこちら。

index.js ステップ③
handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

まず初めに、先ほどコンストラクタで生成した配列のコピーを作成しています(理由は後述)。
次に、クリックされたマスに対応する要素の値を、'X'に変更。
そしてstate.squaresを、値の変更が加えられたコピー配列である(const)squaresに置き換えています。

さて、この時点でSquareコンポーネントは、
"クリックされたらそれに反応してBoard内のhandleClickメソッドを呼び出す&props.valueを表示する"
だけの部品になりました、彼はもう自分で値を保持するような存在ではありません。
ここまできたらSquareコンポーネントのコンストラクタは削除しても大丈夫です。
また、renderメソッドのみを有して、自身のstateを持たないコンポーネントは、「関数コンポーネント」と呼ばれるものに置き換えられるので、こちらも書き換えましょう。

以下、現段階でのSquare/Boardコンポーネントです。

index.js
function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

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

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

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

手番の処理

ここでは、Squareをクリックした際に、“X”と “O” が交互に出てくるように改良を加えます。

デフォルトでは、先手を“X”し指定します。Board のコンストラクタで state の初期値を変えればこのデフォルト値は変更できます。

index.js

class Board extends React.Component {
  constructor(props){
      super(props);
      this.state = {
          squares: Array(9).fill(null),
          xIsNext: true,                         ⬅︎ xIsNextを追加し初期値にtrueを設定
      };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O'; ⬅︎ xIsNextの真偽によって表示する値を変更
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,               ⬅︎ stateの値を反転
    });
  }

まず、BoardのコンストラクタにxIsNextという名の真偽値を設定し、初期値をtrueにします。
その次に、handleClickメソッド内で、Squareに渡すprops.valueに、xIsNextがtrueなら'X'を、falseなら'O'を代入します。
そのあとで、xIsNextの真偽値を反転させます。
この変更により、“X” 側と “O” 側が交互に着手できるようになります。

また、Boardコンポーネントのrenderメソッド内にある “status” テキストも変更して、どちらのプレーヤの手番なのかを表示するようにしましょう。

index.js
render() {
    const 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>
    );
  }
}

スクリーンショット 2020-05-22 12.11.23.png
表示される値が変更されていればOKです。

ゲーム勝者の判定

ゲームが決着して次の手番がなくなった時にどちらが勝利したかを表示しましょう。また、既存のコードではすでに値が入ったSquareを再度クリックすると、値が反転してしまうので、これらの課題を解消していきます。

まずは、ファイル末尾に以下のヘルパー関数をコピーして貼り付けてください。

index.js
function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

このヘルパーは、三目並べの勝利パターンである「タテ3・ヨコ3・ナナメ2」の合計8パターンに対して、同じ値が置かれているかを確認する役目を果たしています。もし勝者がいればその値を返し、いなければnullを返します。

このcalculateWinner(squares)ヘルパーを用いて、Boardコンポーネントのrenderメソッドを書き換えましょう。

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

    return (
      // the rest has not changed

ここではまず、winnerに現状のゲームの結果を代入します。
winnerに値が入っていれば'Winner: 勝者'という文字列をstatusに代入し、winnerの値がnullであれば'Next player: 'X'or'O''をstatusに代入します。

続いて、先ほど述べた「すでに値が入ったSquareを再度クリックすると値が反転してしまう」問題を解消します。
hundleClickメソッドを以下のように書き換えましょう。

index.js
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,
    });
  }

calculateWinner(squares)かsquares[i]のいずれかに値が入っている場合にはreturnします。
こうすることはif文以下の処理が実行される前にメソッドを終了することができるので、値が書き換えられるのを防ぐことができます。

スクリーンショット 2020-05-22 12.40.04.png
スクリーンショット 2020-05-22 12.40.13.png

盤面上部のテキストが変化し、埋まっているSquare(もしくは勝敗決定後)にクリックイベントが発生しなければOKです。

タイムトラベル機能の追加

以前の着手まで「時間を巻き戻す」ことができるようにしましょう。

そのためには、「着手の履歴」を保存しなければなりません、
ここで思い出して欲しいのですが、handleClickメソッドでは、常に元の配列のコピーを作成していました。


handleClick(i) {
    const squares = this.state.squares.slice();

このため、squares の過去のバージョンをすべて保存しておいて、過去の手番をさかのぼることができるようになります。

過去の squares の配列を、history という名前の別の配列に保存しましょう。
この history 配列は初手から最後までの盤面の全ての状態を記憶しており、以下のような構造を持っています。

history = [
  // Before first move
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // After first move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // After second move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

State のリフトアップ、再び

上に示したhistory配列は、Gameコンポーネントで管理することにします。
早速、Game コンポーネントのstateにhistoryを設置しましょう

こうすることで、子であるBoardコンポーネントからsquaresのstateを、取り除くことができます。
Squareコンポーネントにあった「stateをリフトアップ」して Boardコンポーネントに移動したときと全く同様にして、今度は BoardにあるstateをトップレベルのGameコンポーネントにリフトアップしましょう。これにより GameコンポーネントはBoardのデータを完全に制御することになり、history内の過去の手番のデータをBoardに表示させることができるようになります。

まず、Gameコンポーネントの初期stateをコンストラクタ内でセットします。

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

次に、Boardコンポーネントが squaresとonClickプロパティを Gameコンポーネントからpropsで受け取るようにします。
renderSquareメソッド内の{this.state.squares[i]}と{this.handleClick(i)}を以下のように書き換え、Boardコンポーネントのコンストラクタは削除しましょう。

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)} ⬅︎ココ
      />
    );
  }

Gameコンポーネントの内容を更新して、ゲームのステータステキストの決定や表示の際に最新の履歴が使われるようにします。
またGameコンポーネント側から、Boardに送るpropsの内容を設定します。以下にGameコンポーネントに加える変更をまとめて記載します。順を追ってその変更の意味を解説します。

まずは、BoardコンポーネントのhandleClickメソッドを、Gameコンポーネントに移行します。

index.js

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
    };
  }


  ⬇︎BoardのhandleClickを移行
  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,
    });
  }

①:定数historyにstateのhistoryを代入
②:定数currentにhistory配列のうち最新のもの(index番号が最大のもの)を代入
③:定数squaresに、currentに含まれている盤面の状態をコピーして代入

※lengthは要素の数を返すので、起点は1です。対して配列の要素に割り当てられたindex番号は0を起点とするので、その差分を埋めるためにhistory.length 「-1」としています。

④:history配列に、変更が加わった新たな配列を生成し、追加。

続いて、renderメソッドの変更をしていきましょう。

index.js


  ⬇︎renderの内容を変更
  render() {
   const history = this.state.history;
   const current = history[history.length - 1];
   const winner = calculateWinner(current.squares);

   const moves = history.map((step, move) => {
     const desc = move?
      'Go to move #' + move :
      'Go to game start';
     return(
      <li>
       <button onClick={() => this.jumpTo(move)}>{desc}</button>
*      </li>
*     );
   });

    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>{moves}</ol>
        </div>
      </div>
    );
  }
}

①・②:先ほどと同様。
③:定数winnerに、最新の盤面の勝敗状況を代入。
④:定数movesにmap関数を用いて生成されたボタンのリストを代入。
⑤:Boardに渡すpropsを設定。
⑥:④で作ったリストを表示

④のプロセスは私がやや混乱したポイントなので、丁寧に解説します。
見たらわかるという方は飛ばしてくださって構いません。

まずmap関数は、「指定された配列を参照して、特定の処理を施した新たな配列を作り出すメソッド」である
ということを念頭に、このコードを読み解いていきましょう。

ここでmapに引数として渡されているのは関数です。
その関数も引数を取っているのですが、
第一引数に元配列の要素を一つずつ呼び出し、
第二引数にその要素のインデックス番号が呼び出され、
要素一つづつに処理を加えたのちに、新たな配列が生成されます。

つまり、
stepにはhistory[0]からhistory[8]までのオブジェクトが、
moveには0から8までのindex番号が渡されています。

参考:https://teratail.com/questions/192578


const moves = history.map((step, move) => {
     const desc = move?
      'Go to move #' + move :
      'Go to game start';
     return(
      <li>
       <button onClick={() => this.jumpTo(move)}>{desc}</button>
*      </li>
*     );
   });

ここまで理解すれば、このコードを読むことに抵抗は感じないはずです。

ゲームの履歴内にある三目並べのそれぞれの着手に対応して、ボタンを有するリストアイテムを作りました。
ボタンにはonClickハンドラがありthis.jumpTo()というメソッドを呼び出していますが、このメソッドは後ほど実装します。

Gameコンポーネントがゲームのステータステキストを表示するようになったので、対応するコードはBoard内のrenderメソッドから削除しましょう。Boardのrender関数は以下のようになります。

index.js(Boadコンポーネント)

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

スクリーンショット 2020-05-22 17.16.02.png
ボタンリストができればOKです。
また、jumpToメソッドが未定義なので、これらのボタンをクリックするとエラーが発生します。後ほどjumpToメソッドを定義します。

タイムトラベルの実装

着手に応じたボタンリストを先ほど作成しましたが、実はまだ不十分です。
それぞれのリストアイテムをReactが別個のものとして捉えるためには、識別子が必要になるからです。
人間から見れば、ボタンテキストに含まれる数字を見てリストのうちのどのアイテムなのか判断することができますが、Reactにはできません。

例えば、道端にいるスズメを個体毎に見分けるのは至難の技ですが、
別々の色がついたタグが足に括られていたとしたら、おそらく見分けられるでしょう。
Reactにも、このタグを用意する必要があります。
タグはReactにおいてkeyと呼ばれます。

三目並べゲームの履歴内においては、すべての着手にはそれに関連付けられた一意なIDが存在します。
着手はゲームの最中に並び変わったり削除されたり挿入されたりすることはありませんから、
着手のインデックスをkeyとして付与しましょう。

index.js
const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

まず、Gameコンポーネントのコンストラクタで、stateにstepNumber: 0を加えます。
これは、いま何手目の状態を見ているのかを表すのに使います。

index.js
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
    };
  }

次に、GameコンポーネントにjumpToメソッドを定義して、stepNumberが更新されるようにします。
jumpToメソッドの引数には、リストボタンを作成した際のmove(つまり、history内の要素のindex番号)が置かれています。

また、更新しようとしているstepNumberの値が偶数だった場合はxIsNextをtrue に設定します。

index.js
handleClick(i) {
    // this method has not changed
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
    });
  }

  render() {
    // this method has not changed
  }

次に、Gameコンポーネントのrenderを書き換えて、常に最後の着手後の状態をレンダーするのではなく stepNumber によって現在選択されている着手をレンダーするようにします。

index.js
render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    // the rest has not changed

最後に、マス目をクリックしたときに実行されるGameのhandleClickメソッドに、いくつか変更を加えます。

時間を巻き戻した時点でマス目がクリックされた場合にhistoryの配列 と stepNumberを更新する記述をします。

index.js

handleClick(i) {
  const history = this.state.history.slice(0, this.state.stepNumber + 1);
    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
      }]),
    stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

slice(a, b)は、指定した配列のindex番号が、a以上、b未満の要素を持つ配列としてコピーします。
4手目まで進んだ局面があったとします。
0・1・2・3・4*
2手目まで戻りましょう。
0・1・2*・3・4
jumpToメソッドでは、history内の要素を検索して、該当する盤面をレンダリングするだけです。
この状態(stepNumberが2の状態)で新たにマス目がクリックされると、handleClickが動作し、

const history = this.state.history.slice(0, this.state.stepNumber + 1);

の部分によって、0以上3未満の要素を持つ配列のコピーが作成されます。
0・1・2*・3・4
⬇︎ slice(0, this.state.stepNumber + 1)
0・1・2*

こうすることで、現在の手'以降'のデータを持たない新たな配列でゲームをリスタートすることができます。
以前と同じように処理が流れ、新たな一手を追加した盤面のデータがhistoryに加えられます。
setStateのstepNumberに配列の長さを渡し、値を更新しましょう。

タイトルなし.gif
このように、ゲームの再スタートができるようになればOKです。

完成!!

以上でチュートリアルは終了です。お付き合いくださりありがとうございました。

後書き

1人で学習を進めていたら、わかったフリしてただろうなぁとか、前後のニュアンスでなんとなく使っていた関数も改めて調べると知らない点が多くて、普段いかに”なんとなく”で作業しているのかが自覚できました。
何か間違っている点があったら教えてくださると幸いです。

1
3
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
1
3