HTML5テクニカルノート
Create React App 入門 04: ゲームの履歴をさかのぼる
- ID: FN2001003
- Technique: ECMAScript 2015
- Library: React 16.12.0
Create React Appのひな形からチュートリアルと同じマルバツゲームをつくる「Create React App 入門」シリーズ第4回は、新たな機能を加えます。それは、待ったをかけることです。盤面の配置データを履歴でもたせ、何手でもさかのぼれるようにします。
01 ゲームの履歴を残す
盤面のXとOの配置データは、9マスの値を要素とする配列にもたせました。ゲームは多くても9手で終わりますから、9要素の配列9個ですべての指し手の履歴が残せるということです。そこで、クラスApp
(モジュールsrc/components/App.js
のstate
)に定めていた盤面の配置データはつぎのようにオブジェクトにして、それを親配列(history
)の要素として収めることにします。こうすれば、一手ごとに9マスの配置をそれぞれ同じかたちのオブジェクトにして、配列要素に加えられるでしょう。
src/components/App.jsclass 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.jsclass 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■マルバツゲームの動きは変わらない
02 今が何手目かをボタンのリストで示す
履歴(history
)には指し手ごとに履歴データが加わります。そのデータの数だけページに履歴ボタンを加えて、今が何手目かを示しましょう(図001)。render()
メソッドがArray.map()
メソッドでつくっているのは、ボタン(<button>
)を入れ子にした<li>
要素の配列(move
)です。これをメソッドが返すテンプレートに加えれば、配列要素から取り出されて順に差し込まれます。<li>要素に加えたkey
プロパティについては、04「keyプロパティを定める」に項を改めてご説明しますので、少しお待ちください。
src/components/App.jsclass 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.jsclass App extends React.Component { jumpTo(step) { console.log(step); } }
図002■何手目かをボタンのリストで示す
03 盤面の配置をクリックした履歴に戻す
onClick
ハンドラのメソッド(jumpTo()
)は、盤面の配置をクリックした履歴に戻して表示します。といっても、つぎのようにstate
に新たに定めた指し手(stepNumber
)の現在値を変えるだけです。render()
メソッドが履歴からその回のデータを取り出して盤面を描けば配置は戻ります。これにともなって、handleClick()
メソッドにも、手を加えました。Array.slice()
メソッドにより、さかのぼった手からあとのデータは除いて、履歴を改めています。
src/components/App.jsclass 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.jsclass 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■ボタンで履歴がさかのぼれる
書き上がったモジュール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 入門
- Create React App 入門 01: 3×3のマス目をつくる
- Create React App 入門 02: クリックしたマス目にXをつける
- Create React App 入門 03: マルバツで勝ち負けを決める
- Create React App 入門 04: ゲームの履歴をさかのぼる
- Create React App 入門 05: 無駄な処理を省く
作成者: 野中文雄
作成日: 2020年01月19日
Copyright © 2001-2020 Fumio Nonaka. All rights reserved.