React

【入門】Reactチュートリアルをやりながら実践入門〜追加課題編〜

前回、こちらの記事でReactの公式チュートリアルを解説しました。

【入門】Reactチュートリアルをやりながら実践入門業務で開発しているプロジェクトでReactを入れることになりまして、そのためのインプットとしてReactの公式チュートリアルを行いました。Reactには入門として公式のチュートリアルがあります。英語で書かれていますが、この記事では日本語で解説していきます。初心者の方でもわかってもらえるように、押さえておくべき部分が出てきた時にその都度解説を挟むようにしています。...

公式のチュートリアルでは、最後に追加課題が出されています。
今回はその追加課題をやっていこうと思います。

チュートリアル本編で作ったもの

公式のReactチュートリアルで、このようなものを作成しました。

誰でもやってことがある「○×ゲーム」です。公式ページではtic-tac-toeゲーム(三目並べ)と呼ばれています。

機能としては

  • プレイヤーが交互に順番が回ってくる
  • すでに埋まったマスは埋めれない
  • 1列揃った時点で終了
  • 上に現状のプレイヤーが誰か、勝負がついた場合に勝者が通知される
  • 右に履歴がでてきてボタンを押したらその時点に遡ることができる。

というようなものです。

本家の完成版がこちらにのっています。

https://github.com/fkt1993/react-sample-app
前回作成したものはこちらのリポジトリにまとめましたのでそちらでも確認ができます。
今回扱う内容もこちらのリポジトリに追加していきます。

追加課題の内容

公式のチュートリアルではこのように出されています。

1.Display the location for each move in the format (col, row) in the move history list.
2.Bold the currently selected item in the move list.
3.Rewrite Board to use two loops to make the squares instead of hardcoding them.
4.Add a toggle button that lets you sort the moves in either ascending or descending order.
5.When someone wins, highlight the three squares that caused the win.
6.When no one wins, display a message about the result being a draw.

ということなので、日本語で訳すと

1. それぞれの遷移に(col,row)の表示をする
2. 遷移リストの現在選択されているものを太字に
3. Boardをloop2回つかって書いてみる
4. 遷移リストを昇順/降順にする
5. 誰かが勝った時に、3つ揃ったセルをハイライトする
6. 引き分けの時にはdrawを表示

こんなかんじになります。
では1つずつ実装していきましょう。

実際にやってみる

課題1.それぞれの遷移に(col,row)の表示をする

右側に履歴が表示されているかと思いますが、その1つ1つの履歴に現状は「#1」というような表示だったのを「#1(0,1)」のようにどのマスを選択したかがわかるように表示していきます。

この履歴に関してはGameコンポーネントhistoryに格納されている配列を、map関数で表示しています。

なのでこのhistoryにクリックしたマスの情報も加えて格納するように変更していきます。

マスをクリックした際に呼ばれるのが、GameコンポーネントhandleClickメソッドで、こちらでhistory stateを書き換えているので、handleClickメソッドを書き換えます。


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,
      col: (i % 3) + 1, //追加
      row: Math.floor(i / 3) + 1, //追加
    }]),
    xIsNext: !this.state.xIsNext,
    stepNumber: history.length
  });
}

historyに新しい配列の要素を追加する際にcolrowを追加するようにしています。

これをもとにhistoryの内容をレンダリングする部分で、この値を使ってマスの位置を表示するように書き換えます。
Gameコンポーネントrenderメソッド内でhistoryに対してmap関数を通していたのでその部分を書き換えました。


const moves = history.map((step, move) => {
  const desc = move ?
    'Move #' + move + '(' + step.col + ',' + step.row + ')': //変更
    'Game start';
  return (
    <li key={move}>
      <button onclick="{()"> this.jumpTo(move)}>{desc}
    </li>
  );
});

以上です。実際に確認してみましょう。

↓このように座標も表示されるようになりました。

これで1番目の課題は完了です!

課題2. 遷移リストの現在選択されているものを太字に

方針としては、GameコンポーネントstepNumberをステートとして保持しているので、これを利用してstepNumberhistoryindexの役割をするmoveが一致した際にboldというclassを付与します。


const moves = history.map((step, move) => {
  const desc = move ?
    'Move #' + move + '(' + step.col + ',' + step.row + ')':
    'Game start';
  return (
    <li key={move}>
      <button onClick={() => this.jumpTo(move)} className={this.state.stepNumber === move ? 'bold' : '' } >{desc}</button> //変更
    </li>
  );
});

boldクラスがついたときに文字が太字になるようにcssも編集します。


.bold {
  font-weight: bold;
}

以上です。実際に確認してみましょう。
↓このように現在選択されているものが太字になりました。

これで2番目の課題は完了です!

課題3. Boardをloopを2回つかって書いてみる

現状Boardコンポーネントrenderメソッドでは以下のようにハードコーディングされています。


class Board extends React.Component {
  renderSquare(i) {
    return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />;
  }
  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>
    );
  }
}

課題3では、こちらをloopを2回使って書き換えていきます。
具体的にはmap関数を2重で使っていきます。

以下のように書き換えました。


class Board extends React.Component {
  renderSquare(i) {
    return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} key={i}/>; //keyを追加
  }
  render() {
    return (
      <div>
        {
          Array(3).fill(0).map((row, i) => {
            return (
              <div className="board-row" key={i}> //keyを追加
                {
                  Array(3).fill(0).map((col, j) => {
                    return(
                      this.renderSquare(i * 3 + j)
                    )
                  })
                }
              </div>
            )
          })
        }
      </div>
    );
  }
}

ポイントとしてはmap関数を使うときはkeyを加えることです。
なぜkeyを使うかというのは、こちらの記事でまとめているのでわからない場合は参考にしてみてください。

【入門】Reactチュートリアルをやりながら実践入門業務で開発しているプロジェクトでReactを入れることになりまして、そのためのインプットとしてReactの公式チュートリアルを行いました。Reactには入門として公式のチュートリアルがあります。英語で書かれていますが、この記事では日本語で解説していきます。初心者の方でもわかってもらえるように、押さえておくべき部分が出てきた時にその都度解説を挟むようにしています。...

以上で課題3が完成です!

遷移リストを昇順/降順にする

遷移リストの上にボタンを設置して、クリックすると遷移リストが昇順か降順か切り替わるようにしていきます。

方針としては、新たに昇順か降順かどうかを保持するstateを新しく定義して、遷移リストをレンダリングする際にstateの状態によって昇順か降順を切り替える。そしてボタンを1つ設置し、onclickstateを切り替えるというようにしていきます。

まずはボタンを設置します。
遷移リストをレンダリングするGameコンポーネントをいじっていきます。


class Game extends React.Component {
  ︙
  ︙
  render() {
  ︙
  ︙
    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>
          <div><button>ASC⇔DESC</button></div> //追加
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}

これによってまずボタンが設置されました。

次に昇順か降順を保持するstateを増やします。
チュートリアル本編でも出てきたように、stateはなるべく親階層で持つべきでした。

なので、GameコンポーネントisAscというstateを追加します。
isAsctrueの場合は昇順、falseの場合は降順とします。


class Game extends React.Component {
  constructor() {
    super();
    this.state = {
      history: [{
        squares: Array(9).fill(null)
      }],
      xIsNext: true,
      stepNumber: 0,
      isAsc: true //追加
    };
  }
 
 ︙

}

遷移リストをレンダリングする際に、次に追加したisAscをもとに昇順か降順かどうかを考慮するように書き換えます。


class Game extends React.Component {
  ︙

    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>
          <div><button>ASC⇔DESC</button></div>
          <ol>{this.state.asc ? moves : moves.reverse()}</ol> //変更
        </div>
      </div>
    );
  }
}

最後に、isAscを切り替えるメソッドを定義して、ボタンをクリックした際に呼び出すようにします。


class Game extends React.Component {

  ︙

  //追加
  toggleAsc() {
    this.setState({
      asc: !this.state.asc,
    });
  }

  ︙

  render() {
  ︙
    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>
          <div><button onClick={() => this.toggleAsc()}>ASC⇔DESC</button></div> //変更
          <ol>{this.state.asc ? moves : moves.reverse()}</ol>
        </div>
      </div>
    );
  }
}

変更は以上です。
実際にちゃんと動くかどうかを確認していきます。
↓このようにボタンで切り替えれるようになっています。

以上で課題4が完成です!

課題5. 誰かが勝った時に、3つ揃ったセルをハイライトする

方針として、勝者がいるかどうかをcalculateWinner関数で判定していますが、こちらが勝者がいない場合にnullを、いる場合に勝者のプレイヤー名の文字列を返していましたが、同時にどのマスの列が揃ったかの配列も返すようにします。

それをもとに、マスを描画する際に、揃っているマスなのかどうかを判断し、そうであればハイライトする用のclassを付与していきます。

ということで、まずはcalculateWinner関数を変更していきます。


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 {
        winner: squares[a], //変更
        line: [a, b, c]     //変更
      };
    }
  }
  return null;
}

このように列が揃った場合の返り値に揃った列の情報も返すようにしました。

次にGameコンポーネントを書き換えます。
calculateWinnerの返り値を変更したのでそれに伴い修正しています。

加えてBoardコンポーネントに揃った列のマスを渡すようにしています。


class Game extends React.Component {
  ︙

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const settlement = calculateWinner(current.squares); //変更

    let status;
    if (settlement) {
      status = 'Winner: ' + settlement.winner; //変更
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    const moves = history.map((step, move) => {
      const desc = move ?
        'Move #' + move + '(' + step.col + ',' + step.row + ')':
        'Game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)} className={this.state.stepNumber === move ? 'bold' : '' } >{desc}</button>
        </li>
      );
    });

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
            highlightCells={settlement ? settlement.line : []} //追加
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <div><button onClick={() => this.toggleAsc()}>ASC⇔DESC</button></div>
          <ol>{this.state.asc ? moves : moves.reverse()}</ol>
        </div>
      </div>
    );
  }
}

Boardコンポーネントも書き換えます。
揃った列のマスの配列を受け取って、マスをレンダリングする際に、そのマスが揃ったマスに含まれているかどうかを判定し、isHighlightで渡すようにしています。


class Board extends React.Component {
  renderSquare(i, isHighlight = false) { //変更
    return <Square isHighlight={isHighlight} key={i} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />; //変更
  }

  render() {
    return (
      <div>
        {
          Array(3).fill(0).map((row, i) => {
            return (
              <div className="board-row" key={i}>
                {
                  Array(3).fill(0).map((col, j) => {
                    return(
                      this.renderSquare(i * 3 + j, this.props.highlightCells.indexOf(i * 3 + j) !== -1) //変更
                    )
                  })
                }
              </div>
            )
          })
        }
      </div>
    );
  }
}

次にSquareコンポーネントを変更します。
isHighlighttrueのときのみhighlightクラスを付与するようにしています。


function Square(props) {
  return (
    <button className={`square ${props.isHighlight ? 'highlight' : ''}`} onClick={() => props.onClick()}>
      {props.value}
    </button>
  );
}

仕上げにhighlightクラスがついたときに背景色が変わるようにcssを編集します。


.highlight {
  background-color: yellow;
}

以上で修正は完了です。
実際に挙動を確認していきます。
↓このように決着がついたときにハイライトされるようになりました。

これで課題5は完了です!

課題6. 引き分けの時にはdrawを表示

方針としては、課題5と同様にcalculateWinner関数で引き分けかどうかの情報も返すようにします。

まず、calculateWinner関数を以下のように変更していきます。


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 {
        isDraw: false, //追加
        winner: squares[a],
        line: [a, b, c]
      };
    }
  }

  //ここから追加
  if (squares.filter((e) => !e).length === 0) {
    return {
      isDraw: true,
      winner: null,
      line: []
    }
  }
  //ここまで追加

  return null;
}

次にGameコンポーネントrenderメソッドを書き換えていきます。


class Game extends React.Component {
  ︙ 
  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const settlement = calculateWinner(current.squares);

    let status;
    if (settlement) {
      //ここから追加
      if (settlement.isDraw) {
        status = 'Draw';
      } else {
        status = 'Winner: ' + settlement.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)}
            highlightCells={settlement ? settlement.line : []}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <div><button onClick={() => this.toggleAsc()}>ASC⇔DESC</button></div>
          <ol>{this.state.asc ? moves : moves.reverse()}</ol>
        </div>
      </div>
    );
  }
}

変更は以上です。
実際の動作を確認していきましょう。
↓引き分けの際に「Draw」が表示されることが確認できました!

以上で追加課題のすべてが完了です。

まとめ

今回は、公式のreactチュートリアルの追加課題を行っていきました。
難易度としてはそこまで難しくなくて、チュートリアルの本編をちゃんと理解していればできる内容であったかなと思います。

今回のコードは本編も含めてGithubのリポジトリにまとめているので、よかったら参考にしてください。
https://github.com/fkt1993/react-sample-app

この記事が学習の参考になっていただければうれしいです。