サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

React + ES6 入門 04: ゲームの履歴をさかのぼる


「React + ES6 入門」はReact公式サイトの「TUTORIAL」をもとに、ReactおよびJSXの構文に加え、ECMAScript 6(ECMAScript 2015)についても解説します。「React + ES6 入門 03: ゲーム管理のコンポーネントを分ける」では、機能が増やせるようにゲームの管理を別のコンポーネントに分け、盤面の配置のデータを履歴でもたせました。今回は、その履歴をさかのぼって、盤面の配置を戻します。いわば、待ったができるようにするわけです。

01 今が何手目かをリストで表示する

手を加えるのは「React + ES6 入門 03」のコード002「盤面の配置のデータが履歴で納められる」です。すでに、9マスの配置データはゲーム管理のコンポーネント(Game)に履歴で配列(history)に納めました。その数をリストで加えて、今が何手目かを示しましょう(図001)。つぎのように、Array.map()メソッドで配列要素を取り出して、<li>要素として<ol>に定数(moves)で差し込みます。


class Game extends React.Component {

	render() {

		const moves = history.map((step, move) => {
			const desc = move ?
				'Move #' + move :
				'Game start';
			return (
				<li>
					<a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
				</li>
			);
		});
		return (
			<div className="game">

				<div className="game-info">

					<ol>{moves/* TODO */}</ol>
				</div>
			</div>
		);
	}
}

図001■何手目かをリストで示す

図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())も書き替えます。つぎのように、戻した手からあとのデータは除いて、履歴を改めればよいでしょう。


class Game extends React.Component {

	handleClick(i) {

		// const history = this.state.history;
		const history = this.state.history.slice(0, this.state.stepNumber + 1);

	}

}

これで履歴をクリックすると盤面がその手まで戻り、マス目のクリックでその盤面からゲームが進められるようになりました。書き上がったスクリプトはつぎのコード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,
		});
	}
	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] Tutorialの「Showing the Moves」にはkeyプロパティを与えないと、つぎのような警告が示されるとあります。筆者の環境では、この警告は表れませんでした。でも、プロパティは加えておくべきでしょう。

Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of "Game".

作成者: 野中文雄
作成日: 2017年4月17日


Copyright © 2001-2016 Fumio Nonaka.  All rights reserved.