React入門11: タイムトラベル(3)

# はじめに
私自身の復習兼、備忘録的な意味もあり、複数回の記事に渡って React を用いてマルバツゲーム(三目並べ)を開発していきたいと思います。
- [React](https://ja.react.dev/)
- [CodeSandbox(公式)](https://codesandbox.io/)
- [CodeSandbox(非公式)](https://csb-jp.github.io/)

## シリーズの一覧
1. [React入門１: 環境構築 [オンライン版]](https://qiita.com/banana_bancho/items/64fa6ccb58101eadbb63)
   - [React入門1.5: 環境構築 [ローカル版-番外編]](https://qiita.com/banana_bancho/private/30f5d9d2c5eea00ac11c)
1. [React入門２: 盤面の作成](https://qiita.com/banana_bancho/private/c50495b5d4c30555cec3)
1. [React入門３: インタラクションの実装](https://qiita.com/banana_bancho/private/557d14b2413cb0317ce4)
1. [React入門４: リファクタリング [リフトアップ編]](https://qiita.com/banana_bancho/private/81730326c004e52c6aad)
1. [React入門５: リファクタリング [インタラクション編]](https://qiita.com/banana_bancho/private/3f20d23aadb49fe233f8)
1. [React入門６: 手番の実装](https://qiita.com/banana_bancho/private/6bed838f67e575f58b72)
1. [React入門７: ゲームの勝利判定](https://qiita.com/banana_bancho/private/c0eca4e9348edb430561)
1. [React入門８: テキストの実装](https://qiita.com/banana_bancho/private/b841b1ce38482b31753c)
1. [React入門９: タイムトラベル(1)](https://qiita.com/banana_bancho/private/c674872e32fcb52bb2a0)
1. [React入門10: タイムトラベル(2)](https://qiita.com/banana_bancho/private/861398f1fa4efd378938)
1. React入門11: タイムトラベル(3) (今回)

# 目的について
## 全体の目的
React公式のチュートリアルで公開されているマルバツゲームを 3x3 のマスで実装していきます。

https://ja.react.dev/learn/tutorial-tic-tac-toe

## 今回の目的
前回の記事では、手番を進めるたびにボタンを表示するようにしました。今回の記事では、そのボタンをクリックすると、その手番に戻れるようにタイムトラベル機能の核となるプログラムを実装していきます。

# タイムトラベル(3)
次のページで、前回のソースファイルを確認できます。

https://codesandbox.io/p/sandbox/react-tutorial10-k8wwf8

- 前回の内容はコチラから！

https://qiita.com/skm_bnn/items/861398f1fa4efd378938

## ```key``` の実装
現段階では、手番が進むとボタンが表示されるだけの状態です。このとき、コンソールでは次のような警告が出力されています。

```
 Warning: Each child in a list should have a unique "key" prop.

Check the render method of `Game`. See https://reactjs.org/link/warning-keys for more information.
    at li
    at Game (https://zzdf2t.csb.app/src/App.js:94:53)
```

これは ```Game``` コンポーネントで定義した ```moves``` 配列に問題があります。この配列は過去の手番ごとのボタンを ```li``` 要素によってリスト形式で表示されています。この ```li``` 要素は、```Game``` コンポーネントの ```return``` 文にある ```ol``` 要素によって、<INS>番号順</INS> に表示されます。この「番号順」というのは、```li``` 要素に ```key``` プロパティの数値に従います。つまり、上記の警告文は、 ```key``` プロパティが指定されていないため、その「番号順」が不明であることを意味しています。

この警告を解決する最も単純な方法として、```li``` 要素を番号順で表示する ```ol``` 要素ではなく、順不同で表示する ```ul``` 要素で囲むことが挙げられます。

```JSX: App.js の Game コンポーネント
export default function Game() {
  /* 省略 */

  const moves = history.map((squares, move) => (
    <li>
      <button>{move} 番目の手番</button>
    </li>
  ));

  /* 省略 */

  return (
    <>
      <div>
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div>
        <ul>{moves}</ul>
      </div>
    </>
  );
}
```

しかし、これでは手番のボタンが番号順に表示されなくなる恐れがあります。そこで、```ol``` 要素で ```li``` 要素を囲み、各 ```li``` 要素には手番に対応した ```key``` プロパティを渡していきます。App.js の ```Game``` コンポーネントに次の変更を加えます。

- ```moves``` 配列の宣言にある ```li``` 要素のプロパティに手番の番号を渡す
    - ヒント: [```map()``` メソッド](https://qiita.com/skm_bnn/items/861398f1fa4efd378938#タイムトラベル2)
- ```return``` 文で ```moves``` 配列をレンダリングするときに、
```ul``` 要素で囲っている場合は ```ol``` 要素に変更する。

```JSX: App.js の Game コンポーネント
export default function Game() {
  /* 省略 */

  const moves = history.map((squares, move) => (
    <li key={move}>
      <button>{move} 番目の手番</button>
    </li>
  ));


  /* 省略 */

  return (
    <>
      <div>
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div>
        <ol>{moves}</ol>
      </div>
    </>
  );
}
```

これで、警告の内容を解決することができました。

### 過去の手番
盤面を過去の手番に戻す機能を実装する前に、画面に表示している盤面が第何回目の手番であるのかを管理する必要があります。App.js の ```Game``` コンポーネントに次の変更を加えます。

-  ```state``` 型の ```currentMove``` 変数を宣言する
    -  現在、ユーザに表示している盤面が何番目の着手であるのかを管理する
    -  初期値: ```0```
    -  set 関数名: ```setCurrentMove```
-  ```moves``` 配列
-  ```jumpTo()``` 関数を定義する
    -  過去の手番を示すボタンがクリックされると、そのときの盤面を表示する
    -  仮引数名を ```nextMove``` とする
    -  ```state``` 型の ```currentMove``` 変数の値を ```nextMove``` にする
    -  そのときの手番が、「x」であるか否かを設定する
        -  ```state``` 型の ```xIsNext``` 変数の値を更新する
        -  ヒント: 偶数回目の手番が「x」である
-  ```handlePlay()``` 関数を変更する
    -  過去に戻って新たに着手をしたときに、その時点以降の履歴のみを消去する
        -  現在は、過去の盤面情報に新しい盤面を加えることで履歴を構成していた
        -  着手時点までの部分のみを保持するようにする
    -  着手が起きるたびに最新の盤面を示す
        -  ```currentMove``` 変数を最新の履歴にする必要がある
        -  ヒント: 履歴を管理している ```history``` 配列の要素数を利用する
-  表示している盤面を第 ```currentMove``` 番目の手番のときのものに変更する
    -  表示している盤面は ```currentSquares``` 変数によって管理されている

```JSX: App.js の Game コンポーネント
export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [xIsNext, setXIsNext] = useState(true);
  const [currentMove, setCurrentMove] = useState(0);
  const currentSquares = history[currentMove];

  const moves = history.map((squares, move) => (
    <li key={move}>
      <button onClick={() => jumpTo(move)}>{move} 番目の手番</button>
    </li>
  ));

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

  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
    setXIsNext(nextMove % 2 === 0);
  }

  return (
    <>
      <div>
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div>
        <ol>{moves}</ol>
      </div>
    </>
  );
}
```

動作結果を次に示します。次の動作が実現できていることを確認してください。

- 手番を進めると、ボタンが表示される
- ボタンを押すと過去の盤面に戻ることができる
- 過去の盤面に戻った状態で新たに着手すると、以降の盤面を示すボタンが削除される

![output_video.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2844961/bcc8510c-f7f0-f619-5c29-56ee375e78cb.gif)

これで、タイムトラベル機能を実装することができました。

# おわりに
今回は、タイムトラベルを完成させました。次のページにソースファイルを示します。

https://codesandbox.io/p/sandbox/react-tutorial11-zzdf2t

公式で説明されていた内容は以上となります。[第8回の記事](https://qiita.com/skm_bnn/items/b841b1ce38482b31753c#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB) にあるように、案次第でさらに拡張することができます。リファクタリングをしてコードを洗練したり、見た目をもっと華やかにしたり、他の機能を実装したりなど改良できる点は山積みです。

公式で紹介されている内容を次に示します。
> まだ時間がある方や、新しく手に入れた React のスキルを練習したい方向けに、三目並べゲームをさらに改善するためのアイディアをいくつか以下に示します。難易度の低い順にリストアップしています：
> 
> 1. 現在の着手の部分だけ、ボタンではなく “You are at move #…” というメッセージを表示するようにする。
> 1. マス目を全部ハードコードするのではなく、Board を 2 つのループを使ってレンダーするよう書き直す。
> 1. 手順を昇順または降順でソートできるトグルボタンを追加する。
> 1. どちらかが勝利したときに、勝利につながった 3 つのマス目をハイライト表示する。引き分けになった場合は、引き分けになったという結果をメッセージに表示する。
> 1. 着手履歴リストで、各着手の場所を (row, col) という形式で表示する。
>
> 『チュートリアル：三目並べ – React』より引用
> https://ja.react.dev/learn/tutorial-tic-tac-toe#wrapping-up

大変長いシリーズとなりましたが、ここまで記事を読んでいただきありがとうございます。分かりにくい点や誤っている点があればコメントにてお気軽にご連絡ください。