[前回] Django+Reactで学ぶプログラミング基礎(24): Reactチュートリアル(ゲーム手番と勝者の判定)
はじめに
前回は、三目並べゲームを完成させました。
今回は、追加のタイムトラベル機能です。
以前の着手へ時間の巻き戻し
機能です。
今回の内容
- タイムトラベル機能を追加
- 着手の履歴を保存
- 再度
state
をリフトアップ
着手の履歴を保存
-
タイムトラベルに必要な前提条件
- ゲーム状態を保存する
squares
の配列をイミュータブルに扱う- 着手があるたびに
squares
のコピーを作り、元の配列を変更しない
- 着手があるたびに
-
squares
の過去のバージョンをすべて保存することで- 過去の手番をさかのぼることができる
- ゲーム状態を保存する
-
過去の
squares
の配列を、history
という別の配列に保存-
history
配列は初手から最後まで、盤面の全ての状態を表現 - 配列の構造:
-
history = [
// 初期状態
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// 初手
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// 2手目
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
再度state
をリフトアップ
着手履歴state
を、トップレベルのGame
コンポーネントに表示
-
history
というstate
をGame
コンポーネントに置き、Game
コンポーネントがアクセスできるように-
Board
コンポーネントにあるstate
を、トップレベルのGame
コンポーネントにリフトアップ - これにより
Game
コンポーネントはBoard
のデータを完全制御できる -
history
内の過去の手番データをBoard
にレンダリングさせる- レンダリングとは、抽象的なデータから画像/映像/音声などを生成すること
-
コードのリファクタリング
- まず、
Game
コンポーネントの初期state
をコンストラクタ内にセット
src/index.js
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
-
次に、
Board
コンポーネントがGame
コンポーネントから、squares
とonClick
プロパティを受け取るように-
Board
のonClick
ハンドラに、Square
の位置を渡す- どのマス目がクリックされたか伝える
-
-
以下の手順で
Board
コンポーネントを書き換える-
Board
のconstructor
を削除 -
Board
のrenderSquare
で-
this.state.squares[i]
をthis.props.squares[i]
に置き換える -
this.handleClick(i)
をthis.props.onClick(i)
に置き換える
-
-
src/index.js
class Board extends React.Component {
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
-
Game
コンポーネントのrender
関数を更新- ゲームのステータステキストの決定や表示の際、最新履歴が使われるように
src/index.js
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
-
Board
内のrender
メソッドから不要となったコードを削除- リファクタリング後、
Board
のrender
関数:
- リファクタリング後、
src/index.js
render() {
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
- 最後に、
handleClick
メソッドをBoard
からGame
に移動-
Game
コンポーネントのstate
構成が変わったため-
handleClick
の中身も修正が必要
-
-
Game
のhandleClick
メソッドで、新しい履歴エントリをhistory
に追加- ※ エントリ追加に、
push()
メソッドでなく、concat()
を使用する理由- 元の配列をミューテート(書き換え)しないため
- ※ エントリ追加に、
-
src/index.js
handleClick(i) {
const history = this.state.history;
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
}]),
xIsNext: !this.state.xIsNext,
});
}
最終的に、各コンポーネントの構成
-
Game
コンポーネント- ゲームの
state
-
handleClick
メソッド
- ゲームの
-
Board
コンポーネント-
renderSquare
メソッド -
render
メソッド
-
-
アプリをデバッグ起動し(F5)、ブラウザで確認
おわりに
過去の着手履歴を保存するため、コードをリファクタリングしました。
着手履歴の画面表示は、次回に持ち越しとなります。
お楽しみに。