Completing the game(ゲームを完成させる)
ここまでの作業で、三目並べゲームの基本的な構成部品がすべて揃いました。完全に動作するゲームにするためには、盤面上に交互に "X" と "O" を置けるようにすることと、勝者を決めるための方法が必要です。
stateのリフトアップ
現在のところ、各 Square コンポーネントが、ゲームの状態を少しずつ保持している状況です。三目並べゲームで勝者を決めるためには、Board が 9 つの Square コンポーネントそれぞれの現在の state を、何らかの形で知る必要があります。
どのようなアプローチが良いでしょうか? 最初に思いつくのは、Board が各 Square に現在の state がどうなっているか「問い合わせる」というものですね。このアプローチも React では技術的には可能ですが、コードが理解しにくくなり、バグが発生しやすくなり、リファクタリングが困難になってしまうため、お勧めしません。ここでの最善はそうではなく、ゲームの state を各 Square ではなく親の Board コンポーネントに保持させることです。Board コンポーネントは、それぞれの Square に、何を表示するのか props を使って伝えることができます。以前にマス目に数字を渡したときと同じですね。
複数の子コンポーネントからデータを収集したい、あるいは 2 つの子コンポーネント同士で通信したい、と思ったら、代わりに親コンポーネントに共有の state を宣言するようにしてください。親コンポーネントはその state を子コンポーネントに prop 経由で渡すことができます。これにより、子同士および親子間で、コンポーネントが同期されるようになります。
state の親コンポーネントへのリフトアップ(持ち上げ)は、React のコンポーネントのリファクタリングにおいてよく発生します。
この機会に試してみましょう。Board コンポーネントを編集して squares という名前の state 変数を宣言し、そのデフォルト値として 9 つのマス目に対応する 9 個の null を持つ配列を与えるようにしましょう:
// ...
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
// ...
);
}
Array(9).fill(null) は、9 個の要素を持つ配列を作成し、それぞれの要素を null に設定します。それを囲む useState() コールは、state 変数 squares を宣言し、初期値をこの配列にします。配列の各要素は、個々のマス目の値に対応します。後で盤面が埋まってくると、squares 配列は次のような見た目になる予定です:
["O", null, "X", "X", "X", "O", "O", null, null];
そして Board コンポーネントは、レンダーする各 Square に props として value を渡していく必要があります:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} />
<Square value={squares[1]} />
<Square value={squares[2]} />
</div>
<div className="board-row">
<Square value={squares[3]} />
<Square value={squares[4]} />
<Square value={squares[5]} />
</div>
<div className="board-row">
<Square value={squares[6]} />
<Square value={squares[7]} />
<Square value={squares[8]} />
</div>
</>
);
}
次に、Square コンポーネントを編集して、value プロパティを改めて Board コンポーネントから受け取るようにします。同時に、Square コンポーネント自身が value を state で管理していたコードと、ボタンにある onClick プロパティを削除する必要があります。
function Square({ value }) {
return <button className="square">{value}</button>;
}
この時点では、空白の三目並べの盤面が表示されているはずです:
コードは以下のようになっています:
// App.js
import { useState } from "react";
function Square({ value }) {
return <button className="square">{value}</button>;
}
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} />
<Square value={squares[1]} />
<Square value={squares[2]} />
</div>
<div className="board-row">
<Square value={squares[3]} />
<Square value={squares[4]} />
<Square value={squares[5]} />
</div>
<div className="board-row">
<Square value={squares[6]} />
<Square value={squares[7]} />
<Square value={squares[8]} />
</div>
</>
);
}
これで、各 Square は、'X' 、'O'、または空の場合は null の、いずれかの value を props として受け取るようになります。
次に、Square がクリックされたときに起こることを変更しなければいけません。いまや Board コンポーネントが、どのマス目が埋まっているのかを管理しています。Square から Board の state を更新する手段が必要です。state はそれを定義しているコンポーネントにプライベートなものですので、Square から Board の state を直接更新することはできません。
代わりに、Board コンポーネントから Square コンポーネントに関数を渡して、マス目がクリックされたときに Square にその関数を呼び出してもらうようにします。クリックされたときに Square コンポーネントが呼び出す関数から始めましょう。その関数を onSquareClick という名前にします:
function Square({ value }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
次に、onSquareClick 関数を Square コンポーネントの props に追加します。
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
次にこの onSquareClick プロパティを、Board コンポーネント内に handleClick という名前で作る関数に接続します。onSquareClick を handleClick に接続するために、1 番目の Square コンポーネントの onSquareClick プロパティに関数を渡しましょう:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={handleClick} />
//...
);
}
最後に、盤面の情報を保持する state である 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);
}
return (
// ...
)
}
handleClick 関数は、slice() 配列メソッドを使って squares 配列のコピー (nextSquares) を作成します。次に、handleClick は、nextSquares 配列を更新して最初の(インデックス [0] の)マス目に X と書き込みます。
setSquares 関数をコールすることで、React はこのコンポーネントの state に変更があったことを知ります。これにより、squares という state 変数を使用しているコンポーネント (Board)、およびその子コンポーネント(盤面を構成している Square コンポーネントすべて)の再レンダーがトリガされます。
Note
JavaScript はクロージャをサポートしているため、内側の関数(例:handleClick)は外側の関数(例:Board)で定義されている変数や関数にアクセスできます。handleClick 関数は、state である squares を読み取ったり、setSquares メソッドを呼び出したりできます。これらは両方とも Board 関数の内部で定義されているためです。
これで、盤に X を置けるようになりました…が、まだ左上隅のマス目にしか置けません。今の handleClick 関数は、左上のマス目に対応するインデックス (0) で更新するようハードコードされているからです。handleClick を書き換えて、任意のマス目の内容を更新できるようにしましょう。handleClick 関数に、更新するマス目のインデックスを指定する引数 i を追加します:
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 (
// ...
)
}
次に、その i を handleClick に渡す必要があります。以下のように、JSX 内で直接 Square の onSquareClick プロパティを handleClick(0) としたくなるかもしれませんが、これはうまくいきません:
<Square value={squares[0]} onSquareClick={handleClick(0)} />
これがうまくいかない理由は、handleClick(0) の呼び出しが、Board コンポーネントのレンダーの一部として発生してしまうからです。handleClick(0) は、setSquares を呼び出して Board コンポーネントの state を更新するため、Board コンポーネント全体が再レンダーされます。しかし、これにより handleClick(0) が再度実行され、無限ループに陥ります:

なぜこの問題が以前には発生しなかったのでしょう?
onSquareClick={handleClick} のようにしていたときは、props として handleClick 関数を渡していました。呼び出してはいませんでした! しかし、今はその関数をその場で呼び出してしまっているのです。handleClick(0) の括弧の部分に注目してください。だからすぐに実行されてしまうのです。ユーザがクリックするまで、handleClick を呼び出したくないわけです。
これを解決する方法として、handleClick(0) を呼び出す handleFirstSquareClick のような関数を作成し、次に handleClick(1) を呼び出す handleSecondSquareClick のような関数を作成し…のようにしていくことも可能です。これらの関数を onSquareClick={handleFirstSquareClick} のようにして、(呼び出すのではなく)props として渡すことができます。これにより無限ループは解決されるでしょう。
しかし、9 つの異なる関数を定義し、それぞれに名前を付けるのは冗長です。代わりに、次のようにしましょう:
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}
新しい () => 構文に注目してください。この () => handleClick(0) はアロー関数と呼ばれる、関数を短く定義する方法です。マス目ががクリックされると、アロー (=>) の後のコードが実行され、handleClick(0) が呼び出されます。
それでは、残り 8 つの Square のコードも更新して、アロー関数の中から handleClick が呼び出されるようにしましょう。handleClick の各呼び出しの引数が、正しくマス目のインデックスに対応していることを確認してください。
export default function Board() {
// ...
return (
<>
<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>
</>
);
}
これで改めて、盤面上の任意のマス目をクリックして X が置けるようになりました。

ですが今では、state の管理がすべて Board コンポーネントによって行われるようになっています!
コードは、以下のようになっているはずです:
// App.js
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 (
<>
<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>
</>
);
}
state 管理が Board コンポーネントに移動されたので、親である Board コンポーネントは子の Square コンポーネントに props を渡すことで、それらが正しく表示されるようにしています。Square をクリックすると、子である Square コンポーネントが親である Board コンポーネントに盤面の state を更新するように要求します。Board の state が変更されると、Board コンポーネントとすべての Square 子コンポーネントが自動的に再レンダーされます。すべてのマス目の state を Board コンポーネントにまとめて保持しておくことで、この後でゲームの勝者を決めることが可能になります。
ユーザが盤面の左上のマス目をクリックして X を置いた場合を例に、何が起こるのかをおさらいしましょう。
- 左上のマス目をクリックすると、
buttonが props として受け取ったonClick関数が実行されます。Squareコンポーネントはその関数をBoardからonSquareClickプロパティとして受け取っています。Boardコンポーネントはその関数を JSX の中で直接定義しています。その関数は引数0でhandleClickを呼び出します。 handleClickは引数0を使って、squares配列の最初の要素をnullからXに更新します。Boardコンポーネントの state であるsquaresが更新されたので、Boardとそのすべての子が再レンダーされます。これにより、インデックス0であるSquareコンポーネントのvalueプロパティがnullからXに変更されます。
最終的に、クリックによって左上のマス目が空白から X に変わったという結果をユーザが目にすることになります。
Note
DOM の <button> 要素は組み込みのコンポーネントなので、その onClick 属性は、React にとって特別な意味を持っています。Square のような独自コンポーネントの場合、名前は自由に決めることができます。Square の onSquareClick プロパティや Board の handleClick 関数にほかのどんな名前を付けても、コードは同じように動作します。React では、イベントを表す props には onSomething という名前を使い、それらのイベントを処理するハンドラ関数の定義には handleSomething という名前を使うことが一般的です。
ここまでの状態をCodeSandbox に保存しています。わからなくなった方は下記からコピーできます。 https://codesandbox.io/s/tic-tac-toe-part3-1-nl88mf?file=/App.js
なぜイミュータブルが重要なのか
注意
この章ではコードの追加はありません。
immutable という関数型プログラミング由来の概念を説明します。
より詳しくは下記から参照してください。
https://ja.react.dev/learn/updating-objects-in-state
handleClick 内で既存の squares 配列を直接変更するのではなく、.slice() を使ってコピーを作成していたことを思い返してください。その理由を説明するために、イミュータビリティ(不変性, immutability)という概念と、なぜイミュータビリティを学ぶことが重要であるかについてお話しします。
データを変更する方法には、一般的に 2 つのアプローチがあります。1 つ目のアプローチは、データの値を直接 ミューテート(書き換え, mutate) する方法です。2 つ目のアプローチは、望ましい変更が施された新しいコピーで元のデータを置換する方法です。以下は、squares 配列を直接書き換えている例です:
const squares = [null, null, null, null, null, null, null, null, null];
squares[0] = "X";
// `squares` は ["X", null, null, null, null, null, null, null, null]; となる
一方で、以下が squares 配列を書き換えずにデータを変更している例です:
const squares = [null, null, null, null, null, null, null, null, null];
const nextSquares = ["X", null, null, null, null, null, null, null, null];
// `squares` は変更されず, `nextSquares` の先頭要素が 'X' になった
結果は同じですが、元のデータの書き換えを行わないことで、いくつかの利点を得ることができます。
イミュータビリティにより、複雑な機能をはるかに簡単に実装することができます。このチュートリアルの後半では、ゲームの履歴を確認して過去の手に「巻き戻し」ができる、「タイムトラベル」機能を実装することになります。このような機能はゲームに特有のものではなく、アクションの取り消しややり直しはアプリケーションでは一般的な要件です。直接的なデータの書き換えを避けることで、データの過去のバージョンを壊すことなく保持し、後で再利用することができます。
イミュータビリティには、もう 1 つの利点があります。デフォルトでは、親コンポーネントの state が変更されると、すべての子コンポーネントは自動的に再レンダーされます。これには state 変更によって影響を受けていない子コンポーネントも含まれます。再レンダー自体はユーザに気付かれないものですが(積極的に避ける必要はありません!)、パフォーマンス上の理由から、影響を受けていないことが明らかなツリーの一部の再レンダーをスキップしたい場合があります。イミュータビリティにより、コンポーネントがデータが変更されたかどうかを非常に安価に比較することができます。React がコンポーネントの再レンダーをいつ行うかについての詳細は、memo API のリファレンスを参照してください。
手番の処理
さて、この三目並べゲームの重大な欠陥、すなわち "O" がまだ盤面上に出てこないという問題を解決する時間がやってきました。
まず先手がデフォルトで "X" になるようにしましょう。現在の手番を追跡するために、Board コンポーネントにもう 1 つ state を追加しましょう:
function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
// ...
}
プレーヤが着手するたびに、どちらのプレーヤの手番なのかを決める xIsNext(真偽値型)が反転して、ゲームの state が保存されます。Board の handleClick 関数を書き換えて、そこで xIsNext の値を反転させましょう:
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);
}
return (
//...
);
}
これで、異なるマス目をクリックすると X と O が正しく交互に表示されるようになりました!
おや、まだ問題があります。同じマス目を何度もクリックしてみてください:

X が O で上書きされてしまっています! これでも大変興味深い特殊ルールにはなりそうですが、今のところはオリジナルのルールを守りましょう。
マス目に X や O を置く前に、まずそのマス目に既に X や O の値があるかどうか、まだチェックしていません。これは、早期リターン (early return) をすることで修正できます。マス目に既に X または O があるかどうかを確認し、既にある場合は handleClick 関数内から早期 return し、盤面の state が更新されてしまわないようにしましょう。
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}
これで空いているマス目にだけ X や O を追加できるようになりました! ここまでのコードは以下のようになります。
// App.js
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 (
<>
<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>
</>
);
}
ここまでの状態をCodeSandbox に保存しています。わからなくなった方は下記からコピーできます。
https://codesandbox.io/s/tic-tac-toe-part3-3-qh73s9?file=/App.js
勝利の宣言
プレーヤが交互に着手できるようになったので、次は勝者が決まった際やこれ以上ゲームを進められなくなった際に、そのように表示することにしましょう。これを実現するために、9 つのマス目の配列を受け取って勝者を判定し 'X'、'O' または null を返す、calculateWinner という名前のヘルパー関数を追加します。calculateWinner 関数の中身は React 特有のものではありませんので、あまり気にしないようにしてください。
// App.js
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;
}
NotecalculateWinner を Board の前後のどちらで定義しても問題ありません。今はコンポーネントを編集するたびにスクロールしないで済むよう、これを最後に置きましょう。
Board コンポーネントの handleClick 関数で calculateWinner(squares) を呼び出して、いずれかのプレーヤが勝利したかどうか判定します。これは、ユーザがすでに X や O のあるマス目をクリックしたかどうかチェックしている場所と同じところで行えます。どちらの場合でも早期リターンを行いたいです。
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
//...
}
ゲームが終了したことを知らせるために、"Winner: X" または "Winner: O" というテキストを表示しましょう。これを行うため、Board コンポーネントに status の欄を追加します。このステータス欄は、ゲームが終了した場合に勝者を表示し、ゲームが続行中の場合は、次がどちらの手番なのか表示します。
export default function Board() {
// ...
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
// ...
)
}
おめでとうございます! これで、動作する三目並べのゲームができました。そして、あなたが React の基本を学ぶこともできました。本当の勝者はあなたです。ここでのコードは以下のようになっています:
// App.js
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 (calculateWinner(squares) || squares[i]) {
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 (
<>
<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>
</>
);
}
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;
ここまでの状態をCodeSandbox に保存しています。わからなくなった方は下記からコピーできます。
https://codesandbox.io/s/tic-tac-toe-part3-4-wlyz32?file=/App.js