はじめに
私自身の復習兼、備忘録的な意味もあり、複数回の記事に渡って React を用いてマルバツゲーム(三目並べ)を開発していきたいと思います。
シリーズの一覧
- React入門1: 環境構築 [オンライン版]
- React入門2: 盤面の作成
- React入門3: インタラクションの実装
- React入門4: リファクタリング [リフトアップ編]
- React入門5: リファクタリング [インタラクション編]
- React入門6: 手番の実装
- React入門7: ゲームの勝利判定
- React入門8: テキストの実装
- React入門9: タイムトラベル(1)
- React入門10: タイムトラベル(2)
- React入門11: タイムトラベル(3) (今回)
目的について
全体の目的
React公式のチュートリアルで公開されているマルバツゲームを 3x3 のマスで実装していきます。
今回の目的
前回の記事では、手番を進めるたびにボタンを表示するようにしました。今回の記事では、そのボタンをクリックすると、その手番に戻れるようにタイムトラベル機能の核となるプログラムを実装していきます。
タイムトラベル(3)
次のページで、前回のソースファイルを確認できます。
- 前回の内容はコチラから!
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
要素によって、番号順 に表示されます。この「番号順」というのは、li
要素に key
プロパティの数値に従います。つまり、上記の警告文は、 key
プロパティが指定されていないため、その「番号順」が不明であることを意味しています。
この警告を解決する最も単純な方法として、li
要素を番号順で表示する ol
要素ではなく、順不同で表示する ul
要素で囲むことが挙げられます。
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()
メソッド
- ヒント:
-
return
文でmoves
配列をレンダリングするときに、
ul
要素で囲っている場合はol
要素に変更する。
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
変数によって管理されている
- 表示している盤面は
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>
</>
);
}
動作結果を次に示します。次の動作が実現できていることを確認してください。
- 手番を進めると、ボタンが表示される
- ボタンを押すと過去の盤面に戻ることができる
- 過去の盤面に戻った状態で新たに着手すると、以降の盤面を示すボタンが削除される
これで、タイムトラベル機能を実装することができました。
おわりに
今回は、タイムトラベルを完成させました。次のページにソースファイルを示します。
公式で説明されていた内容は以上となります。第8回の記事 にあるように、案次第でさらに拡張することができます。リファクタリングをしてコードを洗練したり、見た目をもっと華やかにしたり、他の機能を実装したりなど改良できる点は山積みです。
公式で紹介されている内容を次に示します。
まだ時間がある方や、新しく手に入れた React のスキルを練習したい方向けに、三目並べゲームをさらに改善するためのアイディアをいくつか以下に示します。難易度の低い順にリストアップしています:
- 現在の着手の部分だけ、ボタンではなく “You are at move #…” というメッセージを表示するようにする。
- マス目を全部ハードコードするのではなく、Board を 2 つのループを使ってレンダーするよう書き直す。
- 手順を昇順または降順でソートできるトグルボタンを追加する。
- どちらかが勝利したときに、勝利につながった 3 つのマス目をハイライト表示する。引き分けになった場合は、引き分けになったという結果をメッセージに表示する。
- 着手履歴リストで、各着手の場所を (row, col) という形式で表示する。
『チュートリアル:三目並べ – React』より引用
https://ja.react.dev/learn/tutorial-tic-tac-toe#wrapping-up
大変長いシリーズとなりましたが、ここまで記事を読んでいただきありがとうございます。分かりにくい点や誤っている点があればコメントにてお気軽にご連絡ください。