📚

精神と時の部屋日記5 | Reactを理解したい

2023/03/12に公開

reactのドキュメントにある三目並べを作っていく。
ドキュメント


三目並べ

  • 目的:Reactの基礎理解

環境構築

npm install
npm start 
  • ここでドキュメント通りにしているとエラーが起こった。
Failed to compile.

./src/App.js
  Line 3:  'React' must be in scope when using JSX  react/react-in-jsx-scope

Search for the keywords to learn more about each error.


import React from 'react';
  • これでセットアップ完了
import React from 'react';

export default function Square() {
  return <button className="square">X</button>;
}
  • 1行目はreactのimport
  • 2行目はSquare関数の定義 jsのexportで外部からimportできるようになってる。exportのmdn
  • 3行目はボタンを作ってる。buttonがJSX要素。jsとhtmlのタグを組み合わせて記述される。

ボードの構築

  • 最終目標は三目並べで9つの正方形が必要。
  • reactのコンポーネントでは横並びに複数のJSX要素があってはいけないので下に加えていく。
return <button className="square">X</button><button className="square">X</button>;
  • これはエラー出るので(returnで返されるのは単一のJSX要素)
<React.Fragment>
	<button className="square">X</button>
	<button className="square">X</button>
	<button className="square">X</button>
</React.Fragment>
  • このように9つのbuttonを生成し、単一の要素としてreturnに返す。
  • これで横に9つのXが生成される。
  • ここでXたちに番号をつけて3つずつdivでまとめて正方形を描けるようにする。
<div className="board-row">
        <button className="square">1</button>
        <button className="square">2</button>
        <button className="square">3</button>
</div>
  • これで表示がこうなるはずだ。
  • 関数名をSquareからBoardに変えとく
export default function Board() {
  • この時点でのコード
import React from 'react';

export default function Board() {
  return (
    <React.Fragment>
      <div className="board-row">
        <button className="square">1</button>
        <button className="square">2</button>
        <button className="square">3</button>
      </div>
      <div className="board-row">
        <button className="square">4</button>
        <button className="square">5</button>
        <button className="square">6</button>
      </div>
      <div className="board-row">
        <button className="square">7</button>
        <button className="square">8</button>
        <button className="square">9</button>
      </div>
    </React.Fragment>
  )
}

データを渡す

  • 正方形をクリックしたときに正方形の値を空からXに変更したい。
  • 正方形を定義するSquare関数を作り、Boardで使い回す。
import React from 'react';

function Square(){
  return <button className="square">1</button>;
}
export default function Board() {
  return (
    <React.Fragment>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
    </React.Fragment>
  )
}

  • こうなってるはず。
  • 11111って羅列されてるからこれを修正して空の正方形を作る
  • コンポーネントとはUIの一部を表す使い回しが可能な部品。Squareコンポーネントは使いまわせる正方形のbuttonを持ってる。
  • propsとはコンポーネントに渡されるデータ
    コンポーネントとprops
  • propsを使用して正方形がそれぞれ持っている値をSquareコンポーネントが持つようにする。
function Square({value}){
  • このvalueがprop
  • このvalueのpropを9つの正方形に使い回すレンダリング。
  • そのまま書くと文字列として認識される
return <button className="square">value</button>;

  • エスケープしてvalueをjsの変数として認識させる。
return <button className="square">{value}</button>;
  • こうなる
  • Boardコンポーネントの各コンポーネントにpropを渡す。
<div className="board-row">
        <Square value="1" />
        <Square value="2"/>
        <Square value="3"/>
</div>

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

  • 押したらXになるように作る
  • handleClick関数をSquare関数の中で定義する
  • クリックしたときにhandleClick関数が実行されるようにSquareコンポーネントから返されたbuttonに加える。
function Square({ value }) {
  function handleClick() {
    console.log("clicked!");
  }
  return (
    <button className="square" onClick={handleClick}>
      {value}
    </button>
  );
}

  • ここでブラウザのコンソールを確認すると、正方形をクリックするごとにclicked!が表示される。ブラウザのコンソールOption + ⌘ + J (macOS の場合)

  • 次に正方形がクリックされたのちにXになるようにしたい。

  • 今Square関数の中にあるvalueというpropをuseState関数に入れて、クリックされるときに状態を変更できるようにする。useState関数

function Square() {
  const [value, setValue] = useState(null);
  function handleClick() {
    console.log("clicked!");
  }
  return (
    <button className="square" onClick={handleClick}>
      {value}
    </button>
  );
}
  • useStateの使い方
const [state, setState] = useState(初期値)
  • 戻り値が2つあって、stateが現在の値。最初のレンダリングでは初期値
  • setStateは現在の値を更新するもの。
  • 例えば数を数えるものが作れる。useState
import {useState} from 'react';

export default function Counter(){
	const[count, setCount] = useState(0);
	
	function handleClick(){
		setCount(count + 1);
	}
	
	return(
		<button onClick={handleClick}>
			あなたは{}回押しました。
		</button>
	);
}
  • valueをuseStateで定義したのでBoard内のコンポーネントからvalue="x"を消す。
export default function Board() {
  return (
    <React.Fragment>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
    </React.Fragment>
  );
}

  • クリックされるとXが表示されるようにする。console.log("clicked");をイベントハンドラーsetValue('X');に変更する。そうすることでクリックした毎にsetValueにXが渡されレンダリングが実行される。
function handleClick() {
    setValue("X");
  }

ゲームを作っていく

  • 三目並べのXOを交互に配置して勝ち負けを判定する
  • ゲームの状態を保存するためにBoard関数の中でuseState関数を用いてpropを管理する。また管理するための新しいコンポーネントを作る。
// ...
export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));
  return (
// ...
  • Array(9).fill(null)で9つの要素をもつ配列を作成し、初期値に全て空を入れる。Boardコンポーネント内のSquareコンポーネントに今作成したpropsを入れていく。
<div className="board-row">
        <Square value={squares[0]} />
        <Square value={squares[1]} />
        <Square value={squares[2]} />
</div>
  • BoardコンポーネントからSquareがpropsを受け取れるようにする。Squareに前設定したuseStateなどの部分を消す
function Square({ value }) {
  return <button className="square">{value}</button>;
}
  • BoardからSquareに関数を渡して、クリックされるたびにその関数を呼ぶようにする。
function Square({ value }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}
  • 次にSquareのpropsにonSquareClick関数を追加する
function Square({ value, onSquareClick }) {
  • onSquareClickプロップをBoardのコンポーネントに入れてhandleClickと名付ける。onSquareClickをhandleClickに接続するために、最初のSquareコンポーネントのonSquareClickプロップに関数を渡す
<Square value={squares[0]} onSquareClick={handleClick} />
  • ボードの状態を保持するsquares配列を更新するためにBoardコンポーネントでhandleClick関数を定義する。
export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick() {
    const nextSquares = squares.slice();
    nextSquares[0] = "X";
    setSquares(nextSquares);
  }
  • handleClick関数はjsのsliceメソッドでsquaresをコピーし、nextSquaresに渡す。そしてhandleClickはコピーされた配列を更新して0にXを追加する。

  • setSquares関数を呼び出すことで、reactにコンポーネントが更新されたことを知らせる。これでSquaresの状態を使用するコンポーネントであるBoardとその子コンポーネントたちが再レンダリングされる。

  • 全てのマスでXに更新できるように、handleClickを変更する。handleClick関数に、更新するマスのインデックスを受け取る引数iを追加

  function handleClick(i) {
    const nextSquares = squares.slice();
    nextSquares[i] = "X";
<Square value={squares[0]} onSquareClick={handleClick(0)} />
  • このようにhandleClick(0)を呼び出すと、Boardコンポーネントのレンダリングの一部となる。しかし、handleClickはsetSquaresを呼び出しboardコンポーネントの状態を変更するので、全体がループしながらレンダリングされる。無限ループ
  • Squareコンポーネントは初回のレンダリング時にonSquareClickを実行し、その結果をonClickに渡すために再度レンダリングする。その後再度レンダリングされてonSquareClickがまた実行されるみたいなループ。このループを止めるために、SquareコンポーネントでonClickに直接関数を渡す。
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
  • ()=>アロー関数ここではhandleClick(0)が呼び出されてる。クリックされるたびに実行される。これを9つ書く。
  • これで全ての四角でクリックするとXが表示される。
  • 現在のコード
import React from "react";
import { useState } from "react";
//-----
function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}
export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    const nextSquares = squares.slice();
    nextSquares[i] = "X";
    setSquares(nextSquares);
  }
  return (
    <React.Fragment>
      <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>
    </React.Fragment>
  );
}
  • nextSquaresを定義するところで、squares.slice()を用いて配列をコピーして保存している。これにより履歴を作ることができ、全てがレンダリングするからデータの変更が確認しやすくなる

Oを追加していく

  • 最初はXと設定する。
  • プレイヤーが移動するたびにブール値true or falseが反転されて次に操作するプレイヤーが決定されるようにする
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);
  }
  • このままではXとOは互いに上書きしてしまうので変更する。空のマスにXかOを追加する。マスが入力されていればreturnするようにする。
  function handleClick(i) {
    if (squares[i]) {
      return;
    }
    const nextSquares = squares.slice()
  • この時点でのコード
import React from "react";
import { useState } from "react";
//-----
function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}
export default function Board() {
  const [xIsnext, setXIsNext] = useState(true);
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    if (squares[i]) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsnext) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    setSquares(nextSquares);
    setXIsNext(!xIsnext);
  }
  return (
    <React.Fragment>
      <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>
    </React.Fragment>
  );
}

勝者宣言

  • 勝ち負けを判定したい
  • 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 squares[a];
    }
  }
  return null;
}
  • ゲームオーバーの知らせと進行中の次のプレイヤー表示を行う。
export default function Board() {
  // ...
  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }

  return (
    <div>
      <div className="status">{status}</div>
      <div className="board-row">
        // ...
  )
}
  • status関数を定義して、もしwinnerがcaluculateWinnerから渡されれば、勝者を出力し、それ以外なら次のプレイヤーを出力。
  • xIsNextはブール値だから、trueならXがfalseならstatusにはOが入るようになっている。
  • 現在のコード
import React from "react";
import { useState } from "react";
//-----
function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}
export default function Board() {
  const [xIsnext, setXIsNext] = useState(true);
  const [squares, setSquares] = useState(Array(9).fill(null));

  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);
  }
  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsnext ? "X" : "O");
  }
  return (
    <React.Fragment>
      <div className="board-row">
        <div className="status">{status}</div>
        <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>
    </React.Fragment>
  );
}

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

タイムトラベルの追加

  • 配列を変更した際にslice()でコピーしているので都度の履歴は保存されている。squaresの過去の配列を保存したhistory関数を作る。
  • 最上位のGameコンポーネントを作って、そこに履歴の保存を行う。Boardのdefaut宣言は消しとく。
export default function Game() {
  return (
    <div className="game">
      <div className="game-board">
        <Board />
      </div>
      <div className="game-info">
        <ol>{/*TODO*/}</ol>
      </div>
    </div>
  );
}
export function Board() {
  • コンポーネントにuseState関数を用いてゲーム履歴を保存していく
export default function Game() {
  const [xIsnext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const currentSquares = history[history.length - 1];
  • 次に、Gameコンポーネント内に、Boardコンポーネントから呼び出されてゲームを更新するためのhandlePlay関数を作成します。xIsNext、currentSquares、handlePlayをBoardコンポーネントにpropsとして渡します。
  • Game内にBoardから呼び出したゲーム更新用の関数を作る。そしてGameないの関数をpropsとしてBoardに渡す。
export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const currentSquares = history[history.length - 1];

  function handlePlay(nextSquares) {
    //todo
  }
  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{/*TODO*/}</ol>
      </div>
    </div>
  );
}

  • xIsNext関数がxIsnextとなっていた。修正してください。orz
  • Boardが受け取るpropsを追加する。プレイヤーが更新する毎にBoardが更新されたsquares配列を呼び出せるonPlay関数を受け取るようにもする。Boardの最初の二行はGameで定義されたので消す。また、Board内のsetSquaresとsetXIsNextの呼び出しもいらない。onPlayの呼び出しに変える。
export function Board(xIsNext, squares, onPlay) {
  function handleClick(i) {
    if (squares[i] || calculateWinner(squares)) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    onPlay(nextSquares);
  }
  • 次にhandlePlay関数を実装する必要がある。前はBoardで更新された配列でsetSquaresを呼んでた。今はsquares配列をonPlayに渡してる。Boardの中でonPlay(nextSquares)がある。
  • handlePlay関数はGameの状態を更新してレンダリングしたい。更新されたsquares配列を新たな履歴項目として追加して履歴を更新するスプレッド演算子
 function handlePlay(nextSquares) {
    setHistory([...history, nextSquares]);
    setXIsNext(!xIsNext);
  }
  • setHistory関数でhistoryに新しい要素であるnextSquares配列を追加してる。history配列の全ての要素を新しい要素にコピーしてnextSquaresを追加。
  • setXIsNext関数を使用してxIsNextを反転している。次のプレイヤーがXならば次のターンはOとなる。
  • 現時点でのコード
import React from "react";
import { useState } from "react";
//-----
function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}
export function Board(xIsNext, squares, onPlay) {
  function handleClick(i) {
    if (squares[i] || calculateWinner(squares)) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    onPlay(nextSquares);
  }
  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }
  return (
    <React.Fragment>
      <div className="board-row">
        <div className="status">{status}</div>
        <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>
    </React.Fragment>
  );
}

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 Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const currentSquares = history[history.length - 1];

  function handlePlay(nextSquares) {
    setHistory([...history, nextSquares]);
    setXIsNext(!xIsNext);
  }
  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{/*TODO*/}</ol>
      </div>
    </div>
  );
}

履歴を表示する

  • array mapメソッドである配列を別の配列に変換する
  • mapを使って履歴を画面上のボタンに変換し、履歴にジャンプできるようにする。
 const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = "Go to move #" + move;
    } else {
      description = "Go to game start";
    }
    return (
      <li>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });
  • history.mapは新しい配列を生成する。mapはsquaresとmoveの2つの引数をとり、squaresは特的の手番のボード配列、moveはその手番の順番を示す整数。

  • returnでボタンを作成し、onClickプロパティはjumpTo関数をラップしたアロー関数を設定。jumpToは引数としてmoveを受け取りそこへジャンプする

  • =>アロー関数を使用することでコールバック関数(ボタンがクリックされたときに呼び出される)を定義でき、今回ではonClickプロパティにjumpToの引数であるmoveを含めることができるから任意の手番にジャンプすることができる。

  • Board関数の引数が正しくなかったのでエラーが出ます。

export function Board({ xIsNext, squares, onPlay }) {
  • これで大丈夫なはず。

keyについて

  • reactはリストのレンダリング時に書くデータを保持する。その際にreactはkeyプロパティを使用して各アイテムを兄弟アイテムから区別する。これによりreactは各アイテムを認識する。keyはコンポーネントと兄弟間で一意である必要がある。
    リストとkeyについて

タイムトラベルの実装

  • このゲームの履歴は一意のIDがある。Moveのindexをkeyとして使える。
const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = "Go to move #" + move;
    } else {
      description = "Go to game start";
    }
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });
  • ユーザがどの配列を見ているのかをGameコンポーネントが理解する必要がある。currentMoveという新しい変数を定義(デフォルトは0)。
  • currentMoveを更新するために、jumpTo関数を更新し、currentMoveを変更する数値が偶数であればxIsNextのブール値をtrueにする
export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const currentSquares = history[history.length - 1];

  function handlePlay(nextSquares) {
    setHistory([...history, nextSquares]);
    setXIsNext(!xIsNext);
  }
  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
    setXIsNext(nextMove % 2 === 0);
  }
  • 過去に遡った際、その時点までの履歴だけを残す。nextSquares配列保持の関数をhistoryの全ての項目の後に追加せず、history.slivce(0, currentMove + 1)の後に追加して履歴を管理する。
  • 常に最新の配列をレンダリングするのではなく、現在選択されている動きをレンダリングするようにする。
export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const currentSquares = history[currentMove];

  function handlePlay(nextSquares) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
    setXIsNext(!xIsNext);
  }

最終クリーンアップ

  • currentMoveが偶数の時はxIsNextはtrueで奇数の時はfalseなのでxIsNextはなくていい。
export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const currentSquares = history[currentMove];
  const xIsNext = currentMove % 2 === 0;

  function handlePlay(nextSquares) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }
  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
  }
  • 最終コード
import React from "react";
import { useState } from "react";
//-----
function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}
export function Board({ xIsNext, squares, onPlay }) {
  function handleClick(i) {
    if (squares[i] || calculateWinner(squares)) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    onPlay(nextSquares);
  }
  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }
  return (
    <React.Fragment>
      <div className="board-row">
        <div className="status">{status}</div>
        <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>
    </React.Fragment>
  );
}

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 Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const currentSquares = history[currentMove];
  const xIsNext = currentMove % 2 === 0;

  function handlePlay(nextSquares) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }
  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
  }
  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = "Go to move #" + move;
    } else {
      description = "Go to game start";
    }
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });
  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </div>
  );
}

  • お疲れ様でした!

Discussion