HTML5テクニカルノート
React + ES6 入門 04: ゲームの履歴をさかのぼる
- ID: FN1704009
- Technique: HTML5 / JavaScript
- Library: React 15.5.4
「React + ES6 入門」はReact公式サイトの「TUTORIAL」をもとに、ReactおよびJSXの構文に加え、ECMAScript 6(ECMAScript 2015)についても解説します。「React + ES6 入門 03: ゲーム管理のコンポーネントを分ける」では、機能が増やせるようにゲームの管理を別のコンポーネントに分け、盤面の配置のデータを履歴でもたせました。今回は、その履歴をさかのぼって、盤面の配置を戻します。いわば、待ったができるようにするわけです。
01 今が何手目かをリストで表示する
手を加えるのは「React + ES6 入門 03」のコード002「盤面の配置のデータが履歴で納められる」です。すでに、9マスの配置データはゲーム管理のコンポーネント(Game)に履歴で配列(history)に納めました。その数をリストで加えて、今が何手目かを示しましょう(図001)。リストの<li>
要素をJSXで記述して、複数配列に入れると、JSXの中で波かっこ{}
参照すればその数だけ<li>
要素が加えられます(「Lists and Keys」参照)。そこでつぎのように、Array.map()
メソッドで配列要素を取り出して、<li>
要素として<ol>
に定数(moves)で差し込みます。<li>
要素に加えたkey
プロパティについては、03「keyプロパティを定める」でご説明します。
class Game extends React.Component { render() { const moves = history.map((step, move) => { const desc = move ? 'Move #' + move : 'Game start'; return ( <li key={move}> <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a> </li> ); }); return ( <div className="game"> <div className="game-info"> <ol>{moves/* TODO */}</ol> </div> </div> ); } }
図001■何手目かをリストで示す
<li>
要素には<a>
要素を加え、リンクの替わりにonClick
ハンドラを与えました。ハンドラのメソッド(jumpTo())はまだ定めていないので、このままクリックするとエラーになります。試すときは、かりのメソッドを書いておくとよいでしょう。つぎのメソッドは、クリックした手の番号をconsole.log()
メソッドで示します。
class Game extends React.Component { jumpTo(step) { console.log(step); } }
02 盤面の配置をクリックした履歴に戻す
ゲーム管理(Game)のコンポーネントにonClick
ハンドラのメソッド(jumpTo())を定めましょう。盤面の配置をクリックした履歴に戻して表示します。そのため、つぎのようにstate
プロパティに加えた変数(stepNumber)で、今何手目なのか覚えておきます。そして、履歴をクリックしたら、setState()
メソッドで変数の値をクリックした番号に書き替えるのです。render()
メソッドが履歴からその回のデータを取り出して盤面を描けば配置は戻ります。
class Game extends React.Component { constructor() { this.state = { stepNumber: 0, }; } handleClick(i) { this.setState({ stepNumber: history.length, }); } jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) ? false : true, }); } render() { const current = history[this.state.stepNumber]; // history.length - 1]; } }
マス目をクリックしてゲームを進めるハンドラのメソッド(handleClick())も書き替えます。つぎのように、Array.slice()
メソッドで戻した手からあとのデータは除いて、履歴を改めればよいでしょう。
class Game extends React.Component { handleClick(i) { // const history = this.state.history; const history = this.state.history.slice(0, this.state.stepNumber + 1); } }
これで履歴をクリックすると盤面がその手まで戻り、マス目のクリックでその盤面からゲームが進められるようになりました。ただし、勝負が決まったあとマス目をどれかクリックすると、コンポーネント(Game)のゲーム終了のプロパティ(isFinished)がtrue
になります。すると、さかのぼった履歴からゲームが続けられません。履歴をさかのぼるメソッド(jumpTo())で、その値はfalse
に戻しましょう。
class Game extends React.Component { jumpTo(step) { this.setState({ isFinished: false }); } }
書き上がったスクリプトはつぎのコード001にまとめました。また、サンプル001にjsdo.itのコードを掲げてあります。
コード001■履歴をクリックすると盤面の表示が戻る
function Square(props) {
return (
<button className="square" onClick={() => props.onClick()}>
{props.value}
</button>
);
}
class Board extends React.Component {
renderSquare(i) {
return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />;
}
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>
);
}
}
class Game extends React.Component {
constructor() {
super();
this.state = {
history: [
{squares: new Array(9)}
],
stepNumber: 0,
xIsNext: true,
isFinished: false
};
}
handleClick(i) {
if (this.state.isFinished) {
return;
}
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)) {
this.setState({isFinished: true});
return;
}
if (squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) ? false : true,
isFinished: false
});
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const squares = current.squares;
const winner = calculateWinner(squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
const moves = history.map((step, move) => {
const desc = move ?
'Move #' + move :
'Game start';
return (
<li key={move}>
<a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
</li>
);
});
return (
<div className="game">
<Board
squares={squares}
onClick={(i) => this.handleClick(i)}
/>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}
ReactDOM.render(
<Game />,
document.getElementById('container')
);
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],
];
const length = lines.length;
for (let i = 0; i < length; i++) {
const [a, b, c] = lines[i];
const player = squares[a];
if (player && player === squares[b] && player === squares[c]) {
return player;
}
}
return null;
}
サンプル001■React 15.5.4 + ES6: Tic tac toe implementing time travel
03 keyプロパティを定める
前掲コード001のゲーム管理のコンポーネント(Game)で、盤面の配置データから複数差し込む<li>
要素にはkey
プロパティが与えてあります。これは、Reactが要素を識別するために求める値です[*1]。JavaScriptコードでこの値は参照できませんし、使うこともありません。けれど、Reactが要素をダイナミックに扱えるように与えておくのです(「Keys」参照)。
class Game extends React.Component { render() { const moves = history.map((step, move) => { return ( <li key={move}> <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a> </li> ); }); }
[*1] 前掲コード001でkey
プロパティを与えないと、つぎのような警告が示されます。ただし、Reactのライブラリがmin版(react.min.js)の場合はこの警告は表れず、ゲームも一応動きました。でも、プロパティは加えておくべきです。
Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of "Game".
- React + ES6: まずは動かしてみる
- React + ES6 入門 01: ゲームの盤面をつくる
- React + ES6 入門 02: マルバツゲームの勝ちを決める
- React + ES6 入門 03: ゲーム管理のコンポーネントを分ける
作成者: 野中文雄
更新日: 2017年5月5日 本文の加筆・補正およびコードの一部修正。
作成日: 2017年4月17日
Copyright © 2001-2016 Fumio Nonaka. All rights reserved.