HTML5テクニカルノート
React + ES6 入門 03: ゲーム管理のコンポーネントを分ける
- ID: FN1704007
- Technique: HTML5 / JavaScript
- Library: React 15.5.0
「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■マルバツゲームの動きは変わらない
コード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
- React + ES6: まずは動かしてみる
- React + ES6 入門 01: ゲームの盤面をつくる
- React + ES6 入門 02: マルバツゲームの勝ちを決める
- React + ES6 入門 04: ゲームの履歴をさかのぼる
作成者: 野中文雄
作成日: 2017年4月14日
Copyright © 2001-2016 Fumio Nonaka. All rights reserved.