React 公式チュートリアル 解説 (第4回)

React

前回のおさらい

前回はボード側でマス目の状態を管理できるようになりました。

マスをクリックすると「X」が入るようになりました。

ターンを考える

ここからはいよいよ「X」だけでなく、「O」も入るようにしていきます。

早速ボードにstateを1つ追加します。

誰のターンかを状態として管理する必要があるからです。

今回は「xIsNext」という名前にします。初期値はtrueで、trueかfalseが入ります。

xIsNextがtrueなら、「X」のターン。

xIsNextがfalseなら、「O」のターン。

という設定にします。

export default function Board() {
  const [xIsNext, setXIsNext] = useState(true); //追加
  const [squares, setSquares] = useState(Array(9).fill(null));
...

イメージはこんな感じです。

  • はじめにマスをクリックする => xIsNextがtrueなので「X」がマスに表示される & 「xIsNext」がfalseに切り替わる
  • 次にマスをクリック => xIsNextがfalseなので「O」がマスに表示される & 「xIsNext」がtrueに切り替わる
  • 次にマスをクリックする => xIsNextがtrueなので「X」がマスに表示される & 「xIsNext」がfalseに切り替わる
  • 繰り返す

では、「setXIsNext」で状態を切り替えましょう。

クリックした時に切り替えたいので、handleClick関数の中に書きます。

export default function Board() {
  const [xIsNext, setXIsNext] = useState(true);
  const [squares, setSquares] = useState(Array(9).fill(null));
  function handleClick(i) {
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    setSquares(nextSquares);
    setXIsNext(!xIsNext);
    }
  ...以下略

まず「xIsNext」で条件分岐を持たせました。ここで「X」か「O」を判定しています。

handleClick関数の最後にsetXIsNextで、ターンの状態を更新します。

「!xIsnext」で、以前とは逆の状態にすることを意味します。

これで実際の画面を見てみます。

異なるマスをクリックすると、「X」と「O」 が交互に表示されます。

しかし、連続で同じマスをクリックすると切り替わってしまいます。

今回は「1度押したマスは変更不可」にしたいので、切り替わらないようにしたいです。

これはどう実現できるでしょうか?

handleClickに追記します。

  function handleClick(i) {
    if (squares[i]) {
      return
    }
    const nextSquares = squares.slice();

押されたマスに値がある場合は、 早期にreturnしています。

このように早期にreturnする手法はReactに限らずよく使われます。

これでマス目の値の変更はできなくなりました。

勝敗判定

ここまでで、交互にマス目に値を埋める機能まで完成しました。

残るは勝敗を判定するのみです。

勝敗判定はReactはあまり関係ないです。

export default function Board() {
  //...
}

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

calculateWinnerという関数を定義します。

定数「lines」は縦横斜めの1列に並んだ3マス(三目揃った状態のマス目)を列挙しており、これをforでループをかけて三目揃っているか確認しています。

勝敗がつくと「O」か「X」が返り、そうでなければnullが返される関数になっています。

この関数をhandleClickの中で呼び出します。

function handleClick(i) {
  if (squares[i] || calculateWinner(squares)) {
    return;
  }

勝敗がついたら、それ以上マスを更新できないようにしたいので早期にreturnしています。

これだと誰が勝ったか分からないので、画面上に出力します。

export default function Board() {
  const [xIsNext, setXIsNext] = useState(true);
  const [squares, setSquares] = useState(Array(9).fill(null));
//ここから追記
  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }
//ここまで
function handleClick(i) {
... 略

  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>

returnの中にdivタグでstatusを表示します。

これで画面で遊んでみましょう!

ゲームが進行中の場合は次のプレイヤーの番が表示され、

ゲームが終了した場合はステータスに勝者が表示されます。

おめでとうございます!🎉🎉🎉

これで、三目並べゲームが完成しました。また、React の基本も学習しました。

だからあなたはここで本当の勝者です!!(笑)

コードは次のようになります。

import { useState } from "react";


function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}


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


export default function Board() {
  const [xIsNext, setXIsNext] = useState(true);
  const [squares, setSquares] = useState(Array(9).fill(null));
  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }
  function handleClick(i) {
    if (squares[i] || calculateWinner(squares)) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    setSquares(nextSquares);
    setXIsNext(!xIsNext);
  }
  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

次回からは「履歴表示機能」を追加していきます。

(只今作成中です)