rikuto tech blog

ゆる〜くやってます

React Tutorialを TypeScriptとHooksで書き直してみた④

手番の処理

ここからBoardコンポーネントを編集して手番ごとにXとOを入れ替えてプロットされるようにします。

編集後のBoardコンポーネントは以下のようになります。

const Board: VFC = () => {
  const [squares, setSquares] = useState<FillSquare[]>(Array(9).fill(null));
  const [xIsNext, setXIsNext] = useState<boolean>(true);

  const handleClick = (i: number): void => {
    const squaresSlice = squares.slice();
    squaresSlice[i] = xIsNext ? 'X' : 'O';
    setSquares(squaresSlice);
    setXIsNext((c) => !c);
  };

  const renderSquare = (i: number): ReactElement => (
    <Square value={squares[i]} onClick={() => handleClick(i)} />
  );

  const status = `Next player: ${xIsNext ? 'X' : 'O'}`;

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

詳しく見ていきます。

まず、stateにxIsNext: booleanを追加し、手番を保持します。以下の行に該当します。

  const [xIsNext, setXIsNext] = useState<boolean>(true);

次に、handleClick関数を編集し、手番ごとにXとOを入れ替えるようにします。

  const handleClick = (i: number): void => {
    const squaresSlice = squares.slice();
    squaresSlice[i] = xIsNext ? 'X' : 'O';
    setSquares(squaresSlice);
    setXIsNext((c) => !c);
  };

xIsNexttrueのときX、falseならOがプロットされます。

注意すべきは、setXIsNext((c) => !c);でしょうか。これはsetXIsNext(!xIsNext);でも正しく動作しますが、これは偶然です。

りあクト!2巻に記載がありますが、state変数はコンポーネントレンダリングごとで一定になるため、state変数を相対的に変更する処理を行うときはラムダ関数で書くべきとされています。

これは具体的にどういうことかというと、例えばxIsNextがXの状態で、setXIsNext(!xIsNext)を連続して2回行う処理

setXIsNext(!xIsNext);
setXIsNext(!xIsNext);

を実行すると、2回反転されるのでxIsNextはXになるはずですが、実際はOになります。これは1回目の処理が2回目の処理で上書きされるので、実質的に1回しか実行されないのと同じになります。

今回はこのような同じ処理を繰り返すことはないので、setXIsNext(!xIsNext)でも正常に動作するのですが、このような事情から、ラムダ式を使っています。

最後に、status変数を書き換えれば終了です。

  const status = `Next player: ${xIsNext ? 'X' : 'O'}`;

これで手番ごとにXとOが入れ替えるようになりました!!

ゲーム勝者の判定

まず、ファイルに勝者を判定する関数をコピペします。

const calculateWinner = (squares: FillSquare[]) => {
  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 += 1) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }

  return null;
};

公式チュートリアルで提供されているものに、引数の型を付しただけです。

次に、handleClick関数を編集し、マスがすでに埋まっているか、勝者が決まったらマスに入力できないようにします。

  const handleClick = (i: number): void => {
    const squaresSlice = squares.slice();

    // 勝者確定かマスが埋まっていたら、クリックしてもマスが変化しないようにする
    if (calculateWinner(squares) || squaresSlice[i]) {
      return;
    }

    squaresSlice[i] = xIsNext ? 'X' : 'O';
    setSquares(squaresSlice);
    setXIsNext((c) => !c);
  };

最後に、statusを次のように書き換え、勝者判定されたら勝者を表示するように変更します。

  const winner = calculateWinner(squares);
  const status = winner
    ? `Winner: ${winner}`
    : `Next player: ${xIsNext ? 'X' : 'O'}`;

完成したコードがこちらです。

これでゲームは完成です。次はタイムトラベル機能を作っていきます!