サイトトップ

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)。リストの<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■何手目かをリストで示す

図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".

作成者: 野中文雄
更新日: 2017年5月5日 本文の加筆・補正およびコードの一部修正。
作成日: 2017年4月17日


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