サイトトップ

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

HTML5テクニカルノート

React + ES6 入門 03: ゲーム管理のコンポーネントを分ける


「React + ES6 入門」はReact公式サイトの「TUTORIAL」をもとに、ReactおよびJSXの構文に加え、ECMAScript 6(ECMAScript 2015)についても解説します。「React + ES6 入門 02: マルバツゲームの勝ちを決める」では、マルバツゲームの9マスの盤に「X」と「O」を交互に加え、並びから勝ちを決めました(サンプル001)。今回はゲームの動きは基本的に変えず、あとで機能が増やせるように、ゲームの管理を別のコンポーネントに分けます。

サンプル001■React 15.5.3 + ES6: Tic tac toe minimal refined

01 盤面のコンポーネントからゲーム管理の機能を別コンボーネントに分ける

手を加えるのは「React + ES6 入門 02」のコード002「マルバツゲームで勝ちを決める」です。ゲームはふたつのコンポーネントでなり立っています。ひとつのマス目(Square)と、9マスで組み立てたゲーム盤(Board)です。ひとつのマス目には手を加えません。ゲームの管理と進行のコンポーネント(Game)を新たにつくり、ゲーム盤から機能を移します。

まずは、stateプロパティを初期化しているコンストラクタは、つぎのようにゲーム盤(Board)からゲーム管理(Game)のコンポーネントにそっくりそのまま移します。ゲーム盤のコンポーネントからは、コンストラクタがなくなるということです。


class Board extends React.Component {
	/* constructor() {

	} */

}
class Game extends React.Component {
	constructor() {
		super();
		this.state = {
			squares: new Array(9),
			xIsNext: true,
			isFinished: false
		};
	}

}

盤面のコンポーネント(Board)には、マス目の要素をつくって返すメソッド(renderSquare())があります。ところが前の手直しで、値を取り出すstateプロパティは、コンストラクタとともにゲーム管理のコンポーネント(Game)がもつことになりました。また、あとでクリックのハンドラのメソッド(handleClick())も移します。そこで、ゲーム管理のコンポーネントは、つぎのようにrender()メソッドで、値とメソッドへの参照をゲーム盤のコンポーネントの要素に変数(squaresとonClick)として与えます。そうすれば、ゲーム盤のコンポーネントは、変数の参照がpropsプロパティから取り出せるのです。


class Board extends React.Component {

	renderSquare(i) {
		// return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />;
		return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />;
	}

}
class Game extends React.Component {

	render() {

		return (
			<div className="game">
				<Board
					squares={this.state.squares}
					onClick={(i) => this.handleClick(i)}
				/>

			</div>
		);

}

盤面のコンポーネント(Board)のrender()メソッドには「X」と「O」の順番や勝者を示す処理が含まれていました。これはつぎのように、ゲーム管理(Game)のコンポーネントに移します。盤面のコンポーネントのrender()メソッドに残すのは、9マスの盤面の要素をつくって返すreturn文だけです。ただし、順番と勝者を示す変数(status)の要素は除き、ゲーム管理のrender()メソッドが返す要素として加えます。


class Board extends React.Component {

	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 className="status">{status}</div>
			<div>
				<div className="board-row">

				</div>
				<div className="board-row">

				</div>
				<div className="board-row">

				</div>
			</div>
		);
	}
}
class Game extends React.Component {

	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 className="game">
				<Board
					squares={this.state.squares}
					onClick={(i) => this.handleClick(i)}
				/>
				<div className="game-info">
					<div>{status}</div>
					<ol>{/* TODO */}</ol>
				</div>
			</div>
		);
	}
}

前述のとおり、クリックのハンドラのメソッド(handleClick())も、ゲーム盤のコンポーネント(Board)からゲーム管理のコンポーネント(Game)に移します。


class Board extends React.Component {

	/* handleClick(i) {

	} */

}
class Game extends React.Component {

	handleClick(i) {
		if (this.state.isFinished) {
			return;
		}
		const squares = this.state.squares.slice();
		if (calculateWinner(squares)) {
			this.setState({isFinished: true});
			return;
		}
		if (squares[i]) {
			return;
		}
		squares[i] = this.state.xIsNext ? 'X' : 'O';
		this.setState({
			squares: squares,
			xIsNext: !this.state.xIsNext
		});
	}

}

あとは、ReactDOM.render()メソッドの第1引数に渡す要素を、ゲーム管理のクラス(Game)に書き替えるだけです。これでゲーム管理の処理がコンポーネントに分けられました。あとで機能を加えたりするときに、管理がしやすくなります。ここまでのJavaScriptの手直しは、以下のコード001にまとめました。


ReactDOM.render(
	// <Board />,
	<Game />,
	document.getElementById('container')
);

コード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 = {
			squares: new Array(9),
			xIsNext: true,
			isFinished: false
		};
	}
	handleClick(i) {
		if (this.state.isFinished) {
			return;
		}
		const squares = this.state.squares.slice();
		if (calculateWinner(squares)) {
			this.setState({isFinished: true});
			return;
		}
		if (squares[i]) {
			return;
		}
		squares[i] = this.state.xIsNext ? 'X' : 'O';
		this.setState({
			squares: squares,
			xIsNext: !this.state.xIsNext
		});
	}
	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 className="game">
				<Board
					squares={this.state.squares}
					onClick={(i) => this.handleClick(i)}
				/>
				<div className="game-info">
					<div>{status}</div>
					<ol>{/* TODO */}</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;
}

02 盤面の配置のデータを履歴でもたせる

機能の追加をにらんで、もうひとつ手を加えます。盤面の「X」と「O」の配置データは、9マスの値を要素とする配列にもたせました。ゲームは多くても9手で終わりますから、9要素の配列9個ですべての盤面の履歴が残せるということです。そこで、ゲーム管理のコンポーネントのstateプロパティに定めていた盤面のデータはつぎのようにオブジェクトにして、それを親配列(history)の要素として納めることにします。こうすれば、一手ごとに9マスの配置をそれぞれ同じかたちのオブジェクトにして、配列要素に加えられるでしょう。


class Game extends React.Component {
	constructor() {

		this.state = {
			// squares: new Array(9),
			history: [
				{squares: new Array(9)}
			],

		};
	}

}

ゲーム管理のコンポーネント(Garme)が9コマの配置データを扱うのは、クリックイベントのハンドラ(handleClick())とrender()メソッドです。どちらも、まずstateプロパティから親配列(history)を参照し、最後の要素のオブジェクト(current)から直近のデータ(squares)を取り出すかたちにしました。クリックイベントのハンドラは、新たな配置データを親配列にArray.concat()メソッドで加えています。Array.push()メソッドを用いないのは、参照もとのオブジェクトをじかに書き替えないためです(「React + ES6 入門 02」01「9マスの値を盤面のコンポーネントにまとめる」参照)。


class Game extends React.Component {

	handleClick(i) {

		const history = this.state.history;
		const current = history[history.length - 1];
		// const squares = this.state.squares.slice();
		const squares = current.squares.slice();

		this.setState({
			// squares: squares,
			history: history.concat([{
				squares: squares
			}]),

		});
	}
	render() {
		const history = this.state.history;
		const current = history[history.length - 1];
		const squares = current.squares;
		// const winner = calculateWinner(this.state.squares);
		const winner = calculateWinner(squares);

		return (
			<div className="game">
				<Board
					squares={/* this.state. */squares}

				/>

			</div>
		);
	}
}

はじめに述べたとおり、ゲームの動きは前のままです。順番と勝者の表示位置だけ少し変わりました(図001)。けれど、ゲームを管理するコンポーネントが分かれ、盤面の配置データは履歴で残っていますので、機能を拡張しやすくなりました。スクリプトは以下のコード002にまとめ、サンプル002をjsdo.itに掲げます。

図001■マルバツゲームの動きは変わらない

図001

コード002■盤面の配置のデータが履歴で納められる


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)}
			],
			xIsNext: true,
			isFinished: false
		};
	}
	handleClick(i) {
		if (this.state.isFinished) {
			return;
		}
		const history = this.state.history;
		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
			}]),
			xIsNext: !this.state.xIsNext
		});
	}
	render() {
		const history = this.state.history;
		const current = history[history.length - 1];
		const squares = current.squares;
		const winner = calculateWinner(squares);
		let status;
		if (winner) {
			status = 'Winner: ' + winner;
		} else {
			status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
		}
		return (
			<div className="game">
				<Board
					squares={squares}
					onClick={(i) => this.handleClick(i)}
				/>
				<div className="game-info">
					<div>{status}</div>
					<ol>{/* TODO */}</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;
}

サンプル002■React 15.5.3 + ES6: Separating game manager as a component


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


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