stateのリフトアップ
先ほどの記事では、Squareにマスの状態(空白、X)を持たせましたが、ゲームとして成り立つためには、状態を一元管理した方がよいです。そのためには、子要素Squareのstateを親要素Boardに持たせるように書き換えます。このように、子要素のstateを親要素に移すことをリフトアップと呼ぶようです。
class Board extends React.Component {
// ここから追加
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
// ここまで追加
renderSquare(i) {
return <Square value={ this.state.squares[i] } />;
}
(以下略)
Boardにコンストラクターを追加し、その中で要素数9の配列(中身はnull)を定義しています。そして、renderSquare(i)の中でSquareに渡す値を、this.state.squares[i]に変更しています。
さて、次の問題は、Squareでボタンが押されたときに、Boardから渡された値を書き換える方法です。Reactでは、親要素から渡された変数propsの中身を書き換えることができません。そこで、親要素の変数を書き換える関数を子要素に渡します。
class Square extends React.Component {
// constructor(){}は削除
render() {
return (
<button className="square" onClick={() => this.props.onClick()} > // onClickの処理先を this.props.onClickに変更
{this.props.value} // 表示する内容は、直接propsの内容を利用
</button>
);
}
}
まず、Squareの中で変数を保持しなくなったので、コンストラクターは削除します。そして、onClick()は this.props.onClick()に変更し、button要素の中身も this.props.value に戻します。
これで、ボタンを押したら親要素Boarderの関数を呼び出すようにできました。Squareには、this.props.valueを表示する機能と、ボタンを押されたらthis.props.onClick()を呼び出す機能しか持っていません。
class Board extends React.Component {
(中略)
// ここから追加
handleClick(i) {
const squares = this.state.squares.slice(); // squarsを保存
squares[i] = 'X'; // squaresの中を変更
this.setState({squares: squares}); // this.state.squaresをsquaresに置き換え
}
// ここまで追加
renderSquare(i) {
return <Square value={ this.state.squares[i] } onClick={() => this.handleClick(i)}/>; // onClick〜を追加
}
(以下略)
Boaderのコンストラクターの後に、handleClick(i){}関数を追加します。そして、SquareにはvalueのほかにonClickも渡すようにします。
ここで、handleClick(i){}関数の中で、this.state.squaresの値をそのまま書き換えることはなく、一度squaresにコピーしてから書き換え、改めてthis.setState({squares: squares})に書き戻しています。これは、変数が書き換えられたことを他のコンポーネントに伝えることが、state直下の値が書き換えられたときのみしか起こらない、と理解しました。
関数コンポーネント
現在Reactはクラスではなく関数コンポーネントで書くことが主流になってきています。とりあえず、状態を持たないSquareを関数コンポーネントに書き換えてみます。
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
ほんのちょっと、プログラムが短くなりました。
対戦相手を作る
今のところ、マスをクリックするとXが表示されましたが、OとXを交互に表示されるようにします。まずは、手番が交互になるように変更します。
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true, // 手番を表す変数を追加
};
}
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext, // 一手ごとに手番を交代
});
}
(以下略)
コンストラクターの中に、xIsNextというBool型の変数を追加しました。そして、handleClick(i)の中で、squares[i]に入れる値をxIsNextの値に応じてXかOかを選択します。さらに、毎回xIsNextのBool値を逆転させます。
ブラウザで確認すると、マスをクリックするたびにOとXが交互に表示されるようになりました。さらに、render()関数の中も書き換えて、次の手番がOかXかを表示させます。
(前略)
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); // 手番表す変数にOXを代入
(以下略)
勝利判定
次に、3マス並んだら勝利という判定を実装します。index.jsファイルの末尾に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;
}
あらかじめ「この3マスが同じ手番だったら3マス並んだことになる」という3個の値の集合を用意し、squaresの中身のうちその3個が同じならその手番(O, X)を返すという関数です。
この関数を、盤を描画するときに呼び出し、その結果に応じて表示を変えます。
class Board extends React.Component {
(中略)
render() {
const winner = calculateWinner(this.state.squares); // どちらが勝者か判定
let status;
if (winner) {
status = 'Winner: ' + winner; // 勝者がいれば、勝者を代入
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); // 勝者がいなければ、次の手番を代入
}
return (
(以下略)
ブラウザで確認すると、同じ手番が3つ並んだところで「Winner: ?」と表示されます。しかし、その後も試合は続きますし、すでにOXが置かれているマスをクリックするとそのマスが置き換わってしまいます。そこを修正します。
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) { // 勝者がいるか、すでに置かれているならreturn
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
(以下略)
これで、三目並べ完成です。