【React】チュートリアルやってみた⑤
環境
Windows10Pro
目次
①Reactのプロジェクトを作成し、Webからアクセスできることを確認する。
②クリックしたマスに×を表示させる。 ③手番を追加し、クリック時に〇と×が交互に表示されるようにする ④勝利判定を実装する。 ⑤履歴機能を実装する。←ここ最終目標
Reactチュートリアルの三目並べゲームを作成する
目標⑤
履歴機能を実装する。
実践
履歴情報を保存する配列を用意する
Boardコンポーネントのsquares配列を、1手番ごとに保存していく必要があります。今回は、historyという名前の配列を用意し、その中にsquares配列を入れる。
// イメージ
history = [
// Before first move
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// After first move
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// After second move
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
stateのリフトアップ
履歴情報を参照するのは、トップレベルのGameコンポーネントです。そのためには、Gameコンポーネントがhistory配列にアクセスできる必要がある。history配列のstateは、Gameコンポーネントが持つようにする。SquareコンポーネントのstateをBoardコンポーネントにリフトアップしたときと同様にBoardコンポーネントのstateをGameコンポーネントにリフトアップする。
// Boardコンポーネント
class Board extends React.Component {
// コンストラクタ
- constructor(props) {
- super(props);
- this.state = {
- squares: Array(9).fill(null),
- xIsNext: true,
- };
- }
// Gameコンポーネント
class Game extends React.Component {
// コンストラクタ
+ constructor(props) {
+ super(props);
+ this.state = {
// 上のイメージ配列
+ history: [{
+ squares: Array(9).fill(null),
+ }],
+ xIsNext: true,
+ };
+ }
squares配列と、onClickイベントは、Gameコンポーネントから受け取るようになるので、
renderSquareメソッドは以下のように修正する必要がある。
renderSquare(i) {
return (
<Square
+ value={this.props.squares[i]}
+ onClick={() => this.props.onClick(i)}
/>
)
}
Gameコンポーネントのrenderメソッドでゲームのメッセージの決定を最新の履歴で行うようにする。
// Gameコンポーネント
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メソッドで対応するコードを削除する。
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>
);
}
最後に、handleClickメソッドをBoardコンポーネントからGameコンポーネントに移動させます。historyを扱うので、コードが異なることに注意する。
// Boardコンポーネント
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,
});
}
// Gameコンポーネント
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,
});
}
履歴の表示
mapメソッドを使って、履歴の数だけボタンを表示するようにする。
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
+ const moves = history.map((step, move) => {
+ const desc = move ?
+ 'Go to move #' + move :
+ 'Go to game start';
+ return (
+ <li>
+ <button onClick={() => this.jumpTo(move)}>{desc}</button>
+ </li>
+ );
+ });
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>{moves}</ol>
</div>
</div>
);
この時点でゲームをプレイすると、1プレイ毎にボタンが増えていくことを確認できる。
ボタンをクリックすると、this.jumpToメソッドが実装されていないエラーが出る。
ディベロッパーツールのコンソールをみると以下のような警告が出る。
keyを選ぶ
動的なリストを構築する場合、keyと呼ばれるReactの特別なプロパティを割り当てる必要がある。renderメソッド内のliタグでkeyを設定する。
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
+ <li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
巻き戻し機能の実装
Gameコンポーネントのstateに、今何番手なのかを保存する変数を加える。
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
+ stepNumber: 0,
xIsNext: true,
};
}
Gameコンポーネントに、手番数を更新し、真偽値を更新するjunpToメソッドを実装する。
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0,
});
}
新たな手番が発生したときに、手番数のstateを更新する必要がある。handleClickメソッドのsetStateでstepNumberを更新する。
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,
}]),
+ stepNumber: history.length,
xIsNext: !this.state.xIsNext,
});
}
無くなった未来を削除する
巻き戻し機能をつかって巻き戻った後に1つ手番を進めた場合、巻き戻る前の未来の盤面の情報は削除しなければならない。そのため、今表示している手番数までの履歴のみを保持するようにする。すなわち、stepNumberを軸に履歴情報を扱うようにする。
handleClick(i) {
+ const history = this.state.history.slice(0, this.state.stepNumber + 1);
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,
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
});
}
render() {
const history = this.state.history;
+ const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
完成
感想
React独自のstateの考え方やrenderの考え方を体験することができた。
リフトアップなどは慣れていく必要があるのでこの後も何かしらか作っていきたい。
Discussion