このチュートリアルでは、小さな三目並べゲームを作成します。このチュートリアルでは、Reactの既存の知識は前提としていません。チュートリアルで学ぶテクニックは、あらゆるReactアプリを構築する上で基本的なものであり、それを完全に理解することで、Reactについて深く理解することができます。
このチュートリアルはいくつかのセクションに分かれています
- チュートリアルのセットアップでは、チュートリアルに従うための開始点を提供します。
- 概要では、Reactの基礎であるコンポーネント、props、および状態について学びます。
- ゲームの完成では、React開発における最も一般的なテクニックを学びます。
- タイムトラベルの追加では、Reactの独自性である強みへのより深い洞察が得られます。
何を構築しますか?
このチュートリアルでは、Reactを使ったインタラクティブな三目並べゲームを作成します。
完成した時の見た目はここで見ることができます
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { 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 ( <> <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> </> ); } export default function Game() { const [history, setHistory] = useState([Array(9).fill(null)]); const [currentMove, setCurrentMove] = useState(0); const xIsNext = currentMove % 2 === 0; const currentSquares = history[currentMove]; 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> ); } 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; }
まだコードが理解できない場合や、コードの構文に慣れていない場合でも、心配はいりません!このチュートリアルの目標は、Reactとその構文を理解するのを助けることです。
チュートリアルを続ける前に、上記の三目並べゲームを試してみることをお勧めします。気づく機能の1つは、ゲームボードの右側に番号付きのリストがあることです。このリストは、ゲームで発生したすべての動きの履歴を提供し、ゲームの進行に合わせて更新されます。
完成した三目並べゲームで遊んだら、スクロールを続けてください。このチュートリアルでは、よりシンプルなテンプレートから始めます。次のステップは、ゲームの構築を開始できるようにセットアップすることです。
チュートリアルのセットアップ
以下のライブコードエディターで、右上のフォークをクリックして、CodeSandboxのウェブサイトを使用して新しいタブでエディターを開きます。CodeSandboxを使用すると、ブラウザでコードを記述し、作成したアプリがユーザーにどのように表示されるかをプレビューできます。新しいタブには、空の正方形とこのチュートリアルのスターターコードが表示されます。
export default function Square() { return <button className="square">X</button>; }
概要
これで準備が整いましたので、Reactの概要を見ていきましょう!
スターターコードの調査
CodeSandboxには、主に3つのセクションがあります。

- Filesセクションには、
App.js
、index.js
、styles.css
のようなファイルと、public
というフォルダがあります。 - コードエディターでは、選択したファイルのソースコードが表示されます。
- ブラウザセクションでは、記述したコードがどのように表示されるかを確認できます。
FilesセクションでApp.js
ファイルが選択されているはずです。コードエディター内のそのファイルの内容は、次のようになっているはずです。
export default function Square() {
return <button className="square">X</button>;
}
ブラウザセクションには、次のようなX印の付いた正方形が表示されるはずです。

それでは、スターターコード内のファイルを見ていきましょう。
App.js
App.js
内のコードは、コンポーネントを作成します。Reactでは、コンポーネントは、ユーザーインターフェースの一部を表す再利用可能なコードの断片です。コンポーネントは、アプリケーションのUI要素のレンダリング、管理、および更新に使用されます。コンポーネントを一行ずつ見て、何が起こっているかを確認しましょう。
export default function Square() {
return <button className="square">X</button>;
}
最初の行では、Square
という関数を定義しています。export
というJavaScriptキーワードは、この関数をこのファイルの外部からアクセスできるようにします。default
キーワードは、コードを使用している他のファイルに対して、これがファイル内のメイン関数であることを伝えます。
export default function Square() {
return <button className="square">X</button>;
}
2行目はボタンを返します。return
というJavaScriptキーワードは、後に続くものが関数の呼び出し元に値として返されることを意味します。<button>
はJSX要素です。JSX要素は、JavaScriptコードと表示したいHTMLタグの組み合わせであり、表示したいものを記述します。className="square"
は、CSSにボタンのスタイルを設定する方法を伝えるボタンのプロパティまたはpropです。X
はボタンの中に表示されるテキストで、</button>
はJSX要素を閉じて、後に続くコンテンツをボタンの内側に配置しないことを示します。
styles.css
CodeSandboxのFilesセクションで、styles.css
というラベルの付いたファイルをクリックしてください。このファイルは、Reactアプリのスタイルを定義します。最初の2つのCSSセレクター(*
およびbody
)はアプリの大部分のスタイルを定義し、.square
セレクターは、className
プロパティがsquare
に設定されているコンポーネントのスタイルを定義します。コードでは、それはApp.js
ファイルのSquareコンポーネントのボタンに一致します。
index.js
CodeSandboxのFilesセクションで、index.js
というラベルの付いたファイルをクリックしてください。このチュートリアルではこのファイルを編集することはありませんが、これはApp.js
ファイルで作成したコンポーネントとWebブラウザとの間の架け橋です。
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
1~5行目は必要なものをすべてまとめています。
- React
- Webブラウザと通信するためのReactのライブラリ(React DOM)
- コンポーネントのスタイル
App.js
で作成したコンポーネント。
ファイルの残りの部分では、すべての部分をまとめ、最終的な製品をpublic
フォルダのindex.html
に注入します。
ボードの構築
App.js
に戻りましょう。ここでは、チュートリアルの残りの時間を費やすことになります。
現在、ボードは1つの正方形しかありませんが、9つ必要です!正方形をコピー&ペーストして、次のように2つの正方形を作成してみます。
export default function Square() {
return <button className="square">X</button><button className="square">X</button>;
}
すると、このエラーが発生します。
<>...</>
を使用しますか?Reactコンポーネントは、2つのボタンのような複数の隣接するJSX要素ではなく、単一のJSX要素を返す必要があります。これを修正するには、フラグメント(<>
と</>
)を使用して、次のように複数の隣接するJSX要素をラップできます。
export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
これで、次のようになります。

すばらしい!あとは数回コピー&ペーストして9つの正方形を追加するだけです。

大変です!マスがすべて1列に並んでいて、盤面に必要なグリッド状になっていません。これを修正するには、div
でマスを列ごとにグループ化し、いくつかのCSSクラスを追加する必要があります。その際、各マスがどこに表示されるかを確認するために、番号を振っておきましょう。
App.js
ファイルで、Square
コンポーネントを次のように更新します。
export default function Square() {
return (
<>
<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>
</>
);
}
styles.css
で定義されているCSSは、className
がboard-row
のdivをスタイルします。これで、スタイルされたdiv
を使ってコンポーネントを列ごとにグループ化したので、三目並べの盤面ができました。

しかし、ここで問題が発生しました。Square
という名前のコンポーネントは、もはや正方形ではありません。これを修正するために、名前をBoard
に変更しましょう。
export default function Board() {
//...
}
この時点で、コードは次のようになっているはずです。
export default function Board() { return ( <> <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> </> ); }
propsを介したデータの受け渡し
次に、ユーザーがマスをクリックしたときに、マスの値を空から「X」に変更したいとします。今のところ、盤面を構築した方法では、マスを更新するコードを9回(マスごとに1回)コピー&ペーストする必要があります!コピー&ペーストする代わりに、Reactのコンポーネントアーキテクチャを使用すると、再利用可能なコンポーネントを作成して、乱雑で重複したコードを回避できます。
まず、最初のマスを定義している行(<button className="square">1</button>
)をBoard
コンポーネントから新しいSquare
コンポーネントにコピーします。
function Square() {
return <button className="square">1</button>;
}
export default function Board() {
// ...
}
次に、Board
コンポーネントを更新して、JSX構文を使用してそのSquare
コンポーネントをレンダリングします。
// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
ブラウザのdiv
とは異なり、独自のコンポーネントであるBoard
とSquare
は、大文字で始まる必要があることに注意してください。
見てみましょう。

大変です!以前の番号付きのマスがなくなってしまいました。今はどのマスも「1」と表示されます。これを修正するには、propsを使用して、各マスが持つべき値を親コンポーネント(Board
)から子(Square
)に渡します。
Square
コンポーネントを更新して、Board
から渡されるvalue
propを読み取ります。
function Square({ value }) {
return <button className="square">1</button>;
}
function Square({ value })
は、Squareコンポーネントにvalue
というpropを渡せることを示します。
次に、各マス内で1
の代わりに、そのvalue
を表示する必要があります。このようにしてみてください。
function Square({ value }) {
return <button className="square">value</button>;
}
おっと、これは望んでいたものではありません。

コンポーネントから「value」という単語ではなく、value
というJavaScript変数をレンダリングしたいのです。JSXから「JavaScriptにエスケープ」するには、中括弧が必要です。このようにJSXでvalue
を中括弧で囲みます。
function Square({ value }) {
return <button className="square">{value}</button>;
}
今のところ、空の盤面が表示されるはずです。

これは、Board
コンポーネントが、まだvalue
propを、レンダリングする各Square
コンポーネントに渡していないためです。これを修正するには、value
propを、Board
コンポーネントによってレンダリングされる各Square
コンポーネントに追加します。
export default function Board() {
return (
<>
<div className="board-row">
<Square value="1" />
<Square value="2" />
<Square value="3" />
</div>
<div className="board-row">
<Square value="4" />
<Square value="5" />
<Square value="6" />
</div>
<div className="board-row">
<Square value="7" />
<Square value="8" />
<Square value="9" />
</div>
</>
);
}
これで、再び数字のグリッドが表示されるはずです。

更新されたコードは次のようになっているはずです。
function Square({ value }) { return <button className="square">{value}</button>; } export default function Board() { return ( <> <div className="board-row"> <Square value="1" /> <Square value="2" /> <Square value="3" /> </div> <div className="board-row"> <Square value="4" /> <Square value="5" /> <Square value="6" /> </div> <div className="board-row"> <Square value="7" /> <Square value="8" /> <Square value="9" /> </div> </> ); }
インタラクティブなコンポーネントを作成する
クリックするとSquare
コンポーネントをX
で埋めましょう。Square
内でhandleClick
という関数を宣言します。次に、Square
から返されるボタンJSX要素のpropsにonClick
を追加します。
function Square({ value }) {
function handleClick() {
console.log('clicked!');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
今、マスをクリックすると、CodeSandboxのブラウザセクションの下部にあるコンソールタブに"clicked!"
というログが表示されるはずです。マスを複数回クリックすると、"clicked!"
が再度ログに記録されます。同じメッセージのコンソールログが繰り返されても、コンソールにはそれ以上の行は作成されません。代わりに、最初の"clicked!"
ログの横にインクリメントカウンターが表示されます。
次のステップとして、Squareコンポーネントにクリックされたことを「記憶」させ、それを「X」マークで埋めたいとします。「記憶」するために、コンポーネントは状態を使用します。
Reactは、コンポーネントから呼び出して「記憶」させることができるuseState
という特別な関数を提供します。Square
の現在の値を状態に保存し、Square
がクリックされたときに変更しましょう。
ファイルの先頭で useState
をインポートしてください。Square
コンポーネントから value
プロップを削除してください。代わりに、Square
の先頭に useState
を呼び出す新しい行を追加してください。これにより、value
という名前の状態変数が返されるようにします。
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
//...
value
は値を格納し、setValue
は値を変更するために使用できる関数です。useState
に渡される null
は、この状態変数の初期値として使用されるため、ここでの value
は null
と等しい状態から始まります。
Square
コンポーネントはプロップを受け取らなくなるため、Board コンポーネントによって作成された9つの Square コンポーネントすべてから value
プロップを削除する必要があります。
// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
次に、クリック時に「X」を表示するように Square
を変更します。console.log("clicked!");
イベントハンドラーを setValue('X');
に置き換えます。これで、Square
コンポーネントは次のようになります。
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
この set
関数を onClick
ハンドラーから呼び出すことで、Reactに、<button>
がクリックされるたびにその Square
を再レンダリングするように指示しています。更新後、Square
の value
は 'X'
になり、ゲームボードに「X」が表示されます。いずれかの Square をクリックすると、「X」が表示されるはずです。

各 Square は独自の状態を持っています。各 Square に格納されている value
は、他のものとは完全に独立しています。コンポーネントで set
関数を呼び出すと、React は内部の子コンポーネントも自動的に更新します。
上記を変更した後、コードは次のようになります。
import { useState } from 'react'; function Square() { const [value, setValue] = useState(null); function handleClick() { setValue('X'); } return ( <button className="square" onClick={handleClick} > {value} </button> ); } export default function Board() { return ( <> <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 Developer Tools
React DevToolsを使用すると、Reactコンポーネントのプロップと状態を確認できます。CodeSandboxの *ブラウザ* セクションの下部にReact DevToolsタブがあります。

画面上の特定のコンポーネントを検査するには、React DevToolsの左上隅にあるボタンを使用します。

ゲームの完了
この時点で、三目並べゲームの基本的な構成要素はすべて揃っています。ゲームを完成させるには、ボードに「X」と「O」を交互に配置し、勝者を決定する方法が必要です。
状態の持ち上げ
現在、各 Square
コンポーネントは、ゲームの状態の一部を保持しています。三目並べゲームで勝者をチェックするには、Board
は、9つの Square
コンポーネントの状態を何らかの方法で知る必要があります。
どのようにアプローチしますか?最初に、Board
が各 Square
にその Square
の状態を「尋ねる」必要があると推測するかもしれません。このアプローチはReactでは技術的に可能ですが、コードが理解しにくくなり、バグが発生しやすく、リファクタリングが難しくなるため、推奨しません。代わりに、最適なアプローチは、各 Square
ではなく、親 Board
コンポーネントにゲームの状態を格納することです。Board
コンポーネントは、各 Square に数値を渡したときのように、プロップを渡すことで、各 Square
に何を表示するかを伝えることができます。
複数の子からデータを収集したり、2つの子コンポーネントを相互に通信させたりするには、代わりに親コンポーネントで共有状態を宣言します。親コンポーネントは、プロップを介してその状態を子に渡し返すことができます。これにより、子コンポーネントは互いに、そして親と同期が保たれます。
親コンポーネントへの状態の持ち上げは、Reactコンポーネントがリファクタリングされる際に一般的です。
この機会に試してみましょう。Board
コンポーネントを編集して、9つのマスに対応する9つの null の配列をデフォルトとする squares
という名前の状態変数を宣言するようにしてください。
// ...
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
// ...
);
}
Array(9).fill(null)
は、9つの要素を持つ配列を作成し、それぞれの要素を null
に設定します。その周りの useState()
の呼び出しは、初期値がその配列に設定された squares
状態変数を宣言します。配列内の各エントリは、マスの値に対応します。後で盤面を埋める際、squares
配列は次のようになります。
['O', null, 'X', 'X', 'X', 'O', 'O', null, null]
これで、Board
コンポーネントは、レンダリングする各 Square
に 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
コンポーネントを編集して、Board
コンポーネントから value
プロパティを受け取るようにします。これには、Square
コンポーネント自身の value
の状態管理と、ボタンの onClick
プロパティを削除する必要があります。
function Square({value}) {
return <button className="square">{value}</button>;
}
この時点で、空の三目並べ盤面が表示されるはずです。

そして、あなたのコードは次のようになるはずです。
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
プロパティを受け取るようになります。
次に、Square
がクリックされたときに何が起こるかを変更する必要があります。Board
コンポーネントは、どのマスが埋まっているかを管理するようになりました。Square
が Board
の状態を更新する方法を作成する必要があります。状態はそれを定義するコンポーネントにプライベートであるため、Square
から直接 Board
の状態を更新することはできません。
代わりに、Board
コンポーネントから Square
コンポーネントに関数を渡し、マスがクリックされたときに Square
にその関数を呼び出させるようにします。まず、Square
コンポーネントがクリックされたときに呼び出す関数から始めます。その関数を onSquareClick
と呼びます。
function Square({ value }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
次に、onSquareClick
関数を Square
コンポーネントのプロパティに追加します。
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
次に、onSquareClick
プロパティを、Board
コンポーネント内の handleClick
という名前の関数に接続します。onSquareClick
を handleClick
に接続するには、最初の 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} />
//...
);
}
最後に、盤面の状態を保持する 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
関数は、JavaScript の slice()
配列メソッドを使用して、squares
配列のコピー(nextSquares
)を作成します。次に、handleClick
は、最初のマス([0]
インデックス)に X
を追加するように nextSquares
配列を更新します。
setSquares
関数を呼び出すと、React にコンポーネントの状態が変更されたことが通知されます。これにより、squares
状態を使用するコンポーネント(Board
)と、その子コンポーネント(盤面を構成する Square
コンポーネント)の再レンダリングがトリガーされます。
これで、盤面に 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 で直接 onSquareClick
プロパティを handleClick(0)
に設定しようとするかもしれませんが、これは機能しません。
<Square value={squares[0]} onSquareClick={handleClick(0)} />
これがうまくいかない理由です。 handleClick(0)
の呼び出しは、ボードコンポーネントのレンダリングの一部になります。 handleClick(0)
は setSquares
を呼び出すことによってボードコンポーネントの状態を変更するため、ボードコンポーネント全体が再度レンダリングされます。しかし、これにより handleClick(0)
が再度実行され、無限ループが発生します。
なぜこの問題が以前に発生しなかったのでしょうか?
onSquareClick={handleClick}
を渡していたときは、handleClick
関数をプロップとして渡していました。呼び出していなかったのです!しかし、今は handleClick(0)
のように括弧が付いていることに気づいてください。これにより、関数がすぐに実行されてしまいます。ユーザーがクリックするまで handleClick
を呼び出したくありません!
handleClick(0)
を呼び出す handleFirstSquareClick
のような関数、 handleClick(1)
を呼び出す handleSecondSquareClick
のような関数などを作成して、これを修正できます。これらの関数を onSquareClick={handleFirstSquareClick}
のようにプロップとして(呼び出すのではなく)渡します。これで無限ループが解決します。
ただし、9 つの異なる関数を定義し、それぞれに名前を付けるのは冗長すぎます。代わりに、これを行いましょう。
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}
新しい () =>
構文に注目してください。ここで、() => handleClick(0)
はアロー関数であり、関数を定義する簡単な方法です。正方形がクリックされると、=>
"アロー" の後のコードが実行され、handleClick(0)
が呼び出されます。
次に、他の 8 つの正方形を更新して、渡すアロー関数から 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 を追加できます。

しかし今回は、すべての状態管理が Board
コンポーネントによって処理されます!
これがコードの見た目です。
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> </> ); }
状態処理が Board
コンポーネントにあるため、親の Board
コンポーネントは、子 Square
コンポーネントにプロップを渡して、正しく表示できるようにします。Square
をクリックすると、子 Square
コンポーネントは親 Board
コンポーネントにボードの状態を更新するように要求します。 Board
の状態が変化すると、Board
コンポーネントとすべての子 Square
が自動的に再レンダリングされます。すべての正方形の状態を Board
コンポーネントに保持することで、将来の勝者を決定できるようになります。
ユーザーがボードの左上の正方形をクリックして X
を追加するときに何が起こるかをまとめましょう。
- 左上の正方形をクリックすると、
Square
からonClick
プロップとして受け取った関数がbutton
で実行されます。Square
コンポーネントは、その関数をBoard
からonSquareClick
プロップとして受け取りました。Board
コンポーネントは、その関数を JSX で直接定義しました。これは、0
の引数を使用してhandleClick
を呼び出します。 handleClick
は、引数(0
)を使用して、squares
配列の最初の要素をnull
からX
に更新します。Board
コンポーネントのsquares
状態が更新されたため、Board
とそのすべての子が再レンダリングされます。これにより、インデックス0
のSquare
コンポーネントのvalue
プロップがnull
からX
に変わります。
最終的に、ユーザーは左上の正方形が空からクリック後に X
に変わったことを確認します。
不変性が重要な理由
handleClick
で、既存の配列を変更する代わりに、squares
配列のコピーを作成するために .slice()
を呼び出していることに注目してください。理由を説明するために、不変性と、不変性を学ぶことが重要な理由について説明する必要があります。
データを変更する方法には、一般的に 2 つのアプローチがあります。最初のアプローチは、データの値を直接変更することによってデータを変更することです。2 番目のアプローチは、必要な変更が加えられた新しいコピーでデータを置き換えることです。 squares
配列を変更した場合の例を次に示します。
const squares = [null, null, null, null, null, null, null, null, null];
squares[0] = 'X';
// Now `squares` is ["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];
// Now `squares` is unchanged, but `nextSquares` first element is 'X' rather than `null`
結果は同じですが、(基盤となるデータを変更することなく)直接変更しないことで、いくつかの利点が得られます。
イミュータビリティ(不変性)は、複雑な機能を実装するのを非常に簡単にします。このチュートリアルの後半では、ゲームの履歴を確認したり、過去の動きに「ジャンプバック」したりできる「タイムトラベル」機能を実装します。この機能はゲームに特有のものではなく、特定のアクションを元に戻したりやり直したりする機能は、アプリの一般的な要件です。直接的なデータ変更を避けることで、データの以前のバージョンをそのまま保持し、後で再利用できます。
イミュータビリティには、もう1つの利点もあります。デフォルトでは、親コンポーネントの状態が変更されると、すべての子コンポーネントが自動的に再レンダリングされます。これには、変更の影響を受けなかった子コンポーネントも含まれます。再レンダリング自体はユーザーには気づかれませんが(積極的に回避しようとするべきではありません!)、パフォーマンス上の理由から、明らかに影響を受けなかったツリーの一部を再レンダリングしないようにしたい場合があります。イミュータビリティを使用すると、コンポーネントはデータが変更されたかどうかを非常に簡単に比較できます。Reactがコンポーネントをいつ再レンダリングするかを決定する方法について詳しくは、memo
APIリファレンスをご覧ください。
手番の交代
この三目並べゲームの重大な欠陥、つまり盤面に「O」をマークできないという問題を修正する時が来ました。
デフォルトでは、最初の動きを「X」に設定します。これを追跡するために、Boardコンポーネントに別の状態を追加しましょう。
function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
// ...
}
プレーヤーが動くたびに、xIsNext
(ブール値)が反転して、次にプレーするプレーヤーが決定され、ゲームの状態が保存されます。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
の値があるかどうかを確認していません。これは早期リターンで修正できます。マスにすでにX
またはO
があるかどうかを確認します。マスがすでに埋まっている場合は、盤面状態を更新しようとする前に、handleClick
関数でreturn
します。
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}
これで、空のマスにのみX
またはO
を追加できます!この時点でのコードは次のようになります。
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> </> ); }
勝者の宣言
プレーヤーが交代できるようになったので、ゲームに勝った時や、もう交代する番がない時に表示するようにします。これを行うには、9つのマスの配列を受け取り、勝者を確認して、適切に'X'
、'O'
、またはnull
を返すcalculateWinner
というヘルパー関数を追加します。calculateWinner
関数についてはあまり気にしないでください。これは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;
}
Board
コンポーネントのhandleClick
関数でcalculateWinner(squares)
を呼び出して、プレーヤーが勝ったかどうかを確認します。このチェックは、ユーザーがすでにX
またはO
があるマスをクリックしたかどうかを確認するのと同時に実行できます。両方の場合で早期にreturnしたいと考えています。
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
//...
}
ゲームが終了したときにプレーヤーに知らせるために、「勝者:X」または「勝者: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の基本も学びました。ですから、あなたが本当の勝者です。コードは次のようになるはずです。
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; }
タイムトラベル機能の追加
最後の演習として、ゲームの前の動きに「時間を戻す」ことができるようにしましょう。
ムーブの履歴の保存
squares
配列をミューテートした場合、タイムトラベルの実装は非常に困難になります。
しかし、あなたは各ムーブの後にslice()
を使用してsquares
配列の新しいコピーを作成し、それをイミュータブルとして扱いました。これにより、squares
配列の過去のすべてのバージョンを保存し、すでに発生したターン間を移動できるようになります。
過去のsquares
配列をhistory
という別の配列に格納します。これは新しい状態変数として格納されます。history
配列は、最初のムーブから最後のムーブまでのすべての盤面状態を表し、次のような形状になります。
[
// Before first move
[null, null, null, null, null, null, null, null, null],
// After first move
[null, null, null, null, 'X', null, null, null, null],
// After second move
[null, null, null, null, 'X', null, null, null, 'O'],
// ...
]
状態を再び引き上げる
過去のムーブのリストを表示するために、Game
という新しいトップレベルコンポーネントを作成します。そこには、ゲーム全体の履歴を含むhistory
状態を配置します。
history
状態をGame
コンポーネントに配置すると、子コンポーネントであるBoard
コンポーネントからsquares
状態を削除できます。Square
コンポーネントからBoard
コンポーネントに「状態を引き上げた」のと同じように、今度はBoard
からトップレベルのGame
コンポーネントに引き上げます。これにより、Game
コンポーネントがBoard
のデータを完全に制御できるようになり、history
から以前のターンをレンダリングするようにBoard
に指示できます。
まず、export default
を使用してGame
コンポーネントを追加します。このコンポーネントでBoard
コンポーネントといくつかのマークアップをレンダリングするようにします。
function Board() {
// ...
}
export default function Game() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
function Board() {
宣言の前にあるexport default
キーワードを削除し、function Game() {
宣言の前に追加することに注意してください。これは、index.js
ファイルに対して、Board
コンポーネントではなく、トップレベルコンポーネントとしてGame
コンポーネントを使用するように指示します。Game
コンポーネントによって返される追加のdiv
は、後で盤面に追加するゲーム情報のためのスペースを確保しています。
Game
コンポーネントに、次のプレイヤーとムーブの履歴を追跡するための状態を追加します。
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
// ...
[Array(9).fill(null)]
は、単一の項目を持つ配列であり、それ自体が9つのnull
の配列であることに注意してください。
現在のムーブのマス目をレンダリングするには、history
から最後のマス目配列を読み取る必要があります。このためにはuseState
は必要ありません。レンダリング中に計算するための十分な情報がすでにあります。
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
// ...
次に、ゲームを更新するためにBoard
コンポーネントによって呼び出されるGame
コンポーネント内にhandlePlay
関数を作成します。xIsNext
、currentSquares
、およびhandlePlay
をBoard
コンポーネントへのpropsとして渡します。
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} />
//...
)
}
Board
コンポーネントを、受け取るpropsによって完全に制御されるようにしましょう。Board
コンポーネントを、xIsNext
、squares
、およびプレイヤーがムーブを行ったときに更新されたマス目配列を使用してBoard
が呼び出すことができる新しいonPlay
関数の3つのpropsを受け取るように変更します。次に、useState
を呼び出すBoard
関数の最初の2行を削除します。
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
//...
}
// ...
}
次に、Board
コンポーネントのhandleClick
内のsetSquares
とsetXIsNext
の呼び出しを、新しいonPlay
関数の単一の呼び出しに置き換え、ユーザーがマスをクリックしたときにGame
コンポーネントがBoard
を更新できるようにします。
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
//...
}
Board
コンポーネントは、Game
コンポーネントから渡されたpropsによって完全に制御されます。ゲームを再び機能させるには、Game
コンポーネントでhandlePlay
関数を実装する必要があります。
handlePlay
は呼び出されたときに何をする必要がありますか?Board
が更新された配列でsetSquares
を呼び出していたことを思い出してください。現在では、更新されたsquares
配列をonPlay
に渡しています。
handlePlay
関数は、再レンダリングをトリガーするために Game
の状態を更新する必要があります。しかし、以前のように呼び出すことができる setSquares
関数はもうありません。この情報を保存するために history
状態変数を使用しているからです。更新された squares
配列を新しい履歴エントリとして追加して、history
を更新する必要があります。また、Board が以前行っていたように、xIsNext
を切り替える必要もあります。
export default function Game() {
//...
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
//...
}
ここで、[...history, nextSquares]
は、history
のすべての項目に続けて nextSquares
を含む新しい配列を作成します。(...history
スプレッド構文 は、「history
のすべての項目を列挙する」と読むことができます。)
たとえば、history
が [[null,null,null], ["X",null,null]]
で、nextSquares
が ["X",null,"O"]
の場合、新しい [...history, nextSquares]
配列は [[null,null,null], ["X",null,null], ["X",null,"O"]]
になります。
この時点で、状態を Game
コンポーネントに移動させ、UI はリファクタリング前とまったく同じように完全に動作するはずです。この時点でのコードは次のようになります。
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { 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 ( <> <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> </> ); } 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> ); } 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; }
過去の着手を表示する
三目並べゲームの履歴を記録しているので、過去の着手の一覧をプレイヤーに表示できるようになりました。
<button>
のような React 要素は、通常の JavaScript オブジェクトです。アプリケーション内で渡すことができます。React で複数のアイテムをレンダリングするには、React 要素の配列を使用できます。
すでに状態に history
着手の配列があるので、それを React 要素の配列に変換する必要があります。JavaScript では、ある配列を別の配列に変換するために、配列の map
メソッドを使用できます。
[1, 2, 3].map((x) => x * 2) // [2, 4, 6]
map
を使用して、着手の history
を画面上のボタンを表す React 要素に変換し、過去の着手に「ジャンプ」するためのボタンの一覧を表示します。Game コンポーネントの history
を map
しましょう。
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);
}
function jumpTo(nextMove) {
// TODO
}
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>
);
});
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>
);
}
以下にコードがどのようになるかを確認できます。開発者ツールコンソールに次のエラーが表示されることに注意してください。
このエラーは次のセクションで修正します。
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { 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 ( <> <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> </> ); } 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); } function jumpTo(nextMove) { // TODO } 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> ); }); 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> ); } 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; }
map
に渡された関数内で history
配列を反復処理すると、squares
引数は history
の各要素を通過し、move
引数は各配列インデックスを通過します:0
、1
、2
、... (ほとんどの場合、実際の配列要素が必要になりますが、着手の一覧をレンダリングするにはインデックスのみが必要です。)
三目並べゲームの履歴の各着手に対して、ボタン <button>
を含むリスト項目 <li>
を作成します。このボタンには、jumpTo
という関数を呼び出す onClick
ハンドラーがあります(まだ実装していません)。
今のところ、ゲームで発生した着手の一覧と、開発者ツールコンソールにエラーが表示されるはずです。「key」エラーが何を意味するのかを説明しましょう。
キーの選択
一覧をレンダリングする場合、React はレンダリングされた各リスト項目に関する情報を格納します。一覧を更新する場合、React は何が変更されたかを判断する必要があります。一覧のアイテムを追加、削除、並べ替え、または更新した可能性があります。
次の状態から
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
次の状態に移行すると想像してください
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
更新されたカウントに加えて、これを読んだ人間はおそらく、アレクサとベンの順序を入れ替え、アレクサとベンの間にクラウディアを挿入したと言うでしょう。しかし、React はコンピュータープログラムであり、あなたが何を意図したのかを知らないため、各リスト項目を兄弟要素と区別するために、各リスト項目にキープロパティを指定する必要があります。データがデータベースからのものであれば、アレクサ、ベン、クラウディアのデータベース ID をキーとして使用できます。
<li key={user.id}>
{user.name}: {user.taskCount} tasks left
</li>
リストが再レンダリングされると、React は各リスト項目のキーを取得し、前のリストの項目で一致するキーを検索します。現在のリストに以前存在しなかったキーがある場合、React はコンポーネントを作成します。現在のリストに以前のリストに存在していたキーがない場合、React は以前のコンポーネントを破棄します。2 つのキーが一致する場合、対応するコンポーネントが移動されます。
キーは、React に各コンポーネントの識別情報を伝えます。これにより、React は再レンダリング間で状態を維持できます。コンポーネントのキーが変更された場合、コンポーネントは破棄され、新しい状態で再作成されます。
key
は、React において特別かつ予約済みのプロパティです。要素が作成される際、React は key
プロパティを抽出し、返された要素に直接キーを格納します。key
が props として渡されるように見えるかもしれませんが、React はどのコンポーネントを更新するかを決定するために key
を自動的に使用します。コンポーネントが、親が指定した key
を問い合わせる方法はありません。
動的なリストを作成する際は、適切なキーを割り当てることを強く推奨します。適切なキーがない場合は、データ構造を再構成することを検討してください。
キーが指定されていない場合、React はエラーを報告し、デフォルトで配列のインデックスをキーとして使用します。配列のインデックスをキーとして使用することは、リストの項目の並べ替えや、リスト項目の挿入/削除を行う際に問題が発生します。key={i}
を明示的に渡すことでエラーは抑制されますが、配列のインデックスと同じ問題が発生するため、ほとんどの場合推奨されません。
キーはグローバルに一意である必要はなく、コンポーネントとその兄弟の間で一意であれば十分です。
タイムトラベルの実装
三目並べゲームの履歴では、過去の各動きには一意の ID が関連付けられています。それは、その動きの連番です。動きが並べ替えられたり、削除されたり、途中に挿入されたりすることはないため、動きのインデックスをキーとして使用しても安全です。
Game
関数では、<li key={move}>
のようにキーを追加できます。レンダリングされたゲームをリロードすると、React の "key" エラーは消えるはずです。
const moves = history.map((squares, move) => {
//...
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { 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 ( <> <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> </> ); } 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); } function jumpTo(nextMove) { // TODO } 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> ); } 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; }
jumpTo
を実装する前に、ユーザーが現在どのステップを見ているかを追跡するために、Game
コンポーネントが必要です。これを行うには、currentMove
という名前の新しい状態変数を定義し、デフォルト値を 0
にします。
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];
//...
}
次に、Game
内の jumpTo
関数を更新して、その currentMove
を更新します。また、currentMove
を変更している数が偶数の場合は、xIsNext
を true
に設定します。
export default function Game() {
// ...
function jumpTo(nextMove) {
setCurrentMove(nextMove);
setXIsNext(nextMove % 2 === 0);
}
//...
}
ここで、マスをクリックしたときに呼び出される Game
の handlePlay
関数に 2 つの変更を加えます。
- 「時間を戻し」、その時点から新しい動きをした場合、その時点までの履歴のみを保持する必要があります。
history
内のすべての項目 (...
スプレッド構文) の後にnextSquares
を追加する代わりに、history.slice(0, currentMove + 1)
内のすべての項目の後に追加して、古い履歴のその部分のみを保持するようにします。 - 動きが行われるたびに、
currentMove
を最新の履歴エントリを指すように更新する必要があります。
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
setXIsNext(!xIsNext);
}
最後に、Game
コンポーネントを修正して、常に最後の動きをレンダリングするのではなく、現在選択されている動きをレンダリングするようにします。
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];
// ...
}
ゲームの履歴のいずれかのステップをクリックすると、そのステップが発生した後のボードの状態を示すように三目並べのボードがすぐに更新されるはずです。
import { useState } from 'react'; function Square({value, onSquareClick}) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { 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 ( <> <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> </> ); } 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); } function jumpTo(nextMove) { setCurrentMove(nextMove); setXIsNext(nextMove % 2 === 0); } 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> ); } 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; }
最終的なクリーンアップ
コードをよく見ると、currentMove
が偶数の場合、xIsNext === true
であり、currentMove
が奇数の場合、xIsNext === false
であることに気づくかもしれません。つまり、currentMove
の値を知っていれば、xIsNext
がどうあるべきかを常に把握できます。
これらの両方を状態に保存する理由はありません。実際には、冗長な状態は常に避けるようにしてください。状態に格納するものを単純化すると、バグが減り、コードが理解しやすくなります。Game
を変更して、xIsNext
を別の状態変数として保存するのではなく、currentMove
に基づいて計算するようにします。
export default function Game() {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
// ...
}
xIsNext
の状態宣言や、setXIsNext
の呼び出しは不要になりました。これで、xIsNext
が currentMove
と同期しなくなる可能性はなくなりました。コンポーネントをコーディング中に間違いを犯した場合でも同様です。
まとめ
おめでとうございます!次のことができる三目並べゲームを作成しました。
- 三目並べをプレイできる。
- プレイヤーがゲームに勝ったときに示す。
- ゲームの進行状況に応じてゲームの履歴を保存する。
- プレイヤーがゲームの履歴をレビューし、ゲームのボードの以前のバージョンを確認できるようにする。
よくできました!React がどのように動作するかを十分に理解できたと感じていただければ幸いです。
最終結果はこちらで確認できます
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { 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 ( <> <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> </> ); } export default function Game() { const [history, setHistory] = useState([Array(9).fill(null)]); const [currentMove, setCurrentMove] = useState(0); const xIsNext = currentMove % 2 === 0; const currentSquares = history[currentMove]; 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> ); } 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; }
時間がある場合や、新しい React スキルを練習したい場合は、三目並べゲームに以下のような改善を加えるアイデアをいくつかご紹介します。難易度の低いものから順に並べています。
- 現在の動きについてのみ、ボタンの代わりに「あなたはムーブ #… にいます」と表示する。
Board
を書き換えて、マスをハードコーディングするのではなく、2 つのループを使用するようにする。- 動きを昇順または降順に並べ替えることができるトグルボタンを追加する。
- 誰かが勝利した場合、勝利の原因となった 3 つのマスをハイライト表示する(また、誰も勝利しなかった場合は、引き分けになったことを示すメッセージを表示する)。
- 動きの履歴リストに、各動きの位置を (行、列) 形式で表示する。
このチュートリアル全体を通して、要素、コンポーネント、props、状態などの React の概念に触れてきました。ゲームを構築する際にこれらの概念がどのように機能するかを確認したので、Thinking in React を参照して、アプリの UI を構築する際に同じ React の概念がどのように機能するかを確認してください。