サイトトップ

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

HTML5テクニカルノート

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


Create React Appのひな形からチュートリアルと同じマルバツゲームをつくる「Create React App 入門」シリーズ第4回は、新たな機能を加えます。それは、待ったをかけることです。盤面の配置データを履歴でもたせ、何手でもさかのぼれるようにします。

01 ゲームの履歴を残す

盤面のXとOの配置データは、9マスの値を要素とする配列にもたせました。ゲームは多くても9手で終わりますから、9要素の配列9個ですべての指し手の履歴が残せるということです。そこで、クラスApp(モジュールsrc/components/App.jsstate)に定めていた盤面の配置データはつぎのようにオブジェクトにして、それを親配列(history)の要素として収めることにします。こうすれば、一手ごとに9マスの配置をそれぞれ同じかたちのオブジェクトにして、配列要素に加えられるでしょう。

src/components/App.js

class App extends React.Component {
	constructor(props) {

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

		};

	}

}

クラスAppが9コマの配置データを扱うのは、クリックイベントのハンドラ(handleClick())render()メソッドです。どちらも、まずstateから親配列(history)を参照し、最後の要素から直近の配置データ(squares)を取り出すかたちにしました。親配列はスプレッド構文...で複製して変数(history)に収めています。参照もとのデータに直に手を入れないためです(「Create React App 入門 02」「データの変更にはイミュータビリティが大切」参照)。

src/components/App.js

class App extends React.Component {

	handleClick(i) {
		const history = [...this.state.history];
		// const squares = [...this.state.squares];
		const squares = history[history.length - 1].squares;

		// const winner = calculateWinner(this.state.squares);
		const winner = calculateWinner(squares);

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

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

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

				/>

			</div>
		);
	}
}

盤面の配置データの構造をオブジェクトの配列で履歴にしたというだけですから、マルバツゲームの動きはこれまでと変わりません(図001)。

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

図001

02 今が何手目かをボタンのリストで示す

履歴(history)には指し手ごとに履歴データが加わります。そのデータの数だけページに履歴ボタンを加えて、今が何手目かを示しましょう(図001)。render()メソッドがArray.map()メソッドでつくっているのは、ボタン(<button>)を入れ子にした<li>要素の配列(move)です。これをメソッドが返すテンプレートに加えれば、配列要素から取り出されて順に差し込まれます。<li>要素に加えたkeyプロパティについては、04「keyプロパティを定める」に項を改めてご説明しますので、少しお待ちください。

src/components/App.js

class App extends React.Component {

	render() {

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

				/>
				<div className="game-info">

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

ボタンのonClickハンドラに定めるのは、履歴をさかのぼるメソッド(jumpTo)です。コードに問題がないことを確かめたいときには、メソッドにconsole.log()でも仮置きしてください。

src/components/App.js

class App extends React.Component {

	jumpTo(step) {
		console.log(step);
	}

}

図002■何手目かをボタンのリストで示す

図002

03 盤面の配置をクリックした履歴に戻す

onClickハンドラのメソッド(jumpTo())は、盤面の配置をクリックした履歴に戻して表示します。といっても、つぎのようにstateに新たに定めた指し手(stepNumber)の現在値を変えるだけです。render()メソッドが履歴からその回のデータを取り出して盤面を描けば配置は戻ります。これにともなって、handleClick()メソッドにも、手を加えました。Array.slice()メソッドにより、さかのぼった手からあとのデータは除いて、履歴を改めています。

src/components/App.js

class App extends React.Component {
	constructor(props) {

		this.state = {

			stepNumber: 0,

		};

	}
	handleClick(i) {
		// const history = [...this.state.history];
		const history = this.state.history.slice(0, this.state.stepNumber + 1);

		this.setState({

			stepNumber: history.length,

		});
	};
	jumpTo(step) {
		this.setState({
			stepNumber: step,
			xIsNext: (step % 2) === 0,
			finished: false
		});
	}
	render() {

		// const squares = history[history.length - 1].squares;
		const squares = [...history[this.state.stepNumber].squares];

	}
}

履歴ボタンに示すテキストも、Reactのチュートリアルに合わせて書き替えましょう(図003)。ボタンで履歴がさかのぼれるようになりました。

src/components/App.js

class App extends React.Component {

	render() {

		const moves = history.map((step, move) => {
			const desc = move ?
				// 'Move #' + move :
				'Go to move #' + move :
				// 'Game start';
				'Go to game start';

		});

	}
}

図003■ボタンで履歴がさかのぼれる

図002

書き上がったモジュールsrc/components/App.jsのスクリプト全体は、つぎのコード001にまとめたとおりです。また、CodeSandboxに以下のサンプル001を掲げました。

コード001■ゲームの履歴がさかのぼれる

src/components/App.js

import React from 'react';
import Board from './Board';
import './App.css';

class App extends React.Component {
	constructor(props) {
		super(props);
		this.state = {
			history: [
				{squares: new Array(9)}
			],
			stepNumber: 0,
			xIsNext: true,
			finished: false
		};
		this.handleClick = this.handleClick.bind(this);
	}
	handleClick(i) {
		const history = this.state.history.slice(0, this.state.stepNumber + 1);
		const squares = history[history.length - 1].squares;
		if (squares[i]) { return; }
		if (this.state.finished) { return; }
		const winner = calculateWinner(squares);
		if (winner) {
			this.setState({finished: true});
			return;
		}
		squares[i] = this.state.xIsNext ? 'X' : 'O';
		this.setState({
			history: [...history, {squares}],
			stepNumber: history.length,
			xIsNext: !this.state.xIsNext
		});
	};
	jumpTo(step) {
		this.setState({
			stepNumber: step,
			xIsNext: (step % 2) === 0,
			finished: false
		});
	}
	render() {
		const history = [...this.state.history];
		const squares = [...history[this.state.stepNumber].squares];
		const winner = calculateWinner(squares);
		const status = (winner) ?
			'Winner: ' + winner :
			'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
		const moves = history.map((step, move) => {
			const desc = move ?
				'Go to move #' + move :
				'Go to game start';
			return (
				<li key={move}>
					<button onClick={() => this.jumpTo(move)}>{desc}</button>
				</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>
		);
	}
}
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;
}

export default App;

サンプル001■Create React App: Tic Tac Toe 04

04 keyプロパティを定める

前述項02で、履歴(history)の配置データから複数差し込む<li>要素にはkeyプロパティを与えました。このプロパティが加えられていないと、Reactの開発時にはつぎのような警告が示されます。

Warning: Each child in a list should have a unique "key" prop.

配列から動的に要素を加える場合、数が増えたり減ったり、あるいは順番が変わるかもしれません。keyはそのとき、Reactが要素を識別するために求める一意の値です。もっとも、JavaScriptコードでこの値は参照できませんし、使うこともありません。Reactが内部的に要素をダイナミックに扱えるように与えておくのです(「keyを選ぶ」参照)。

keyの値に配列インデックスを使うのは注意が必要

React公式チュートリアルによれば、「配列のインデックスを key として使うことは、項目を並び替えたり挿入/削除する際に問題の原因となります」(「keyを選ぶ」)。一意のkeyは、Reactが要素の動的な変更を追いかけるための仕組みです。問題は配列の途中の要素が除かれたときで、あとの要素のインデックスが繰り上がります。keyが振り直されると、Reactは要素の変更を識別する手段がなくなってしまうのです。

けれど、今回の作例では履歴をさかのぼってやり直しても、あとのボタンが消えるだけで、すでにある要素のkeyは書き替わりません。Reactのチュートリアルの作例でも、keyの値として配列インデックスが用いられています。なお、「keyが指定されなかった場合、Reactは警告を表示し、デフォルトでkeyとして配列のインデックスを使用します」(「keyを選ぶ」)。また、「keyはグローバルに一意である必要はありません。コンポーネントとその兄弟の間で一意であれば十分です」(同前)。

Create React Appのひな形からチュートリアルと同じマルバツゲームをつくることはできました。けれど、もうひとつだけ記事を加えます。次回最終回のテーマは、無駄な処理を省く「断捨離」です。

Create React App 入門


作成者: 野中文雄
作成日: 2020年01月19日


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