React チュートリアル
背景・目標
業務でReactを使用することになったので、Reactの公式ページのチュートリアルを行うことにしました。三目並べゲームの完成を目指します。
環境
- OS:Microsoft Windows 10 Pro
- ターミナル:Windows Subsystem for Linux (Ubuntu 18.04.1 LTS)
- Reactのバージョン:v16.7.0
- ブラウザ:Google Chrome 71.0.3578.98(Official Build) (64 ビット)
公式ページのチュートリアル
Tutorial: Intro to React:https://reactjs.org/tutorial/tutorial.html#setup-option-2-local-development-environment
チュートリアルの準備
チュートリアルを行う環境ですが、2パターン用意されており、ブラウザでコードを書いていくパターンと、ローカル環境で書いていくパターンがあります。今回は、後者のローカル環境でコードを書いていくことにします。
ローカル環境のセットアップ
1.最新バージョンのNode.js
がインストールされていることを確認してください。インストールがまだの場合は、インストールしてください。
node -v
v10.15.0
2.以下のコマンドを実行して、新しいプロジェクトを作成します
npx create-react-app my-app
3.src/
フォルダの中のファイルをすべて削除します
cd my-app
cd src
rm -f *
cd ..
4.index.css
という名前のファイルを、src/
フォルダに作成し、以下の内容を記述します。
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
}
ol, ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
5.index.js
という名前のファイルを、src/
フォルダに作成し、以下の内容を記述します。
class Square extends React.Component {
render() {
return (
<button className="square">
{/* TODO */}
</button>
);
}
}
class Board extends React.Component {
renderSquare(i) {
return <Square />;
}
render() {
const status = 'Next player: X';
return (
<div>
<div className="status">{status}</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 {
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
// ========================================
ReactDOM.render(
<Game />,
document.getElementById('root')
);
6.5で作成した、index.jsの先頭に、以下の行を追加します。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
ここで、ターミナル上でnpm start
を実行し、ブラウザでhttp://localhost:3000
へアクセスすると、三目並べゲームのフィールドが表示されることを確認できます。以上で、準備は完了です。
Reactとは
Reactは宣言的、効率的、そして柔軟なユーザーインターフェース構築用JavaScriptライブラリです。「components」と呼ばれる小さく独立したコードを組み合わせることで、複雑なUIを構成することができます。
Reactは、様々な種類のComponentsを持っていますが、まずは、React.Component
のサブクラスから始めることにしましょう。以下のコードを見てください。
class ShoppingList extends React.Component {
render() {
return (
<div className="shopping-list">
<h1>Shopping List for {this.props.name}</h1>
<ul>
<li>Instagram</li>
<li>WhatsApp</li>
<li>Oculus</li>
</ul>
</div>
);
}
}
// Example usage: <ShoppingList name="Mark" />
Componetは画面に表示したいものを、Reactに伝えるために使用します。データが変更されると、ReactはComponentを効率的に更新して、再レンダリングします。
上記のコードでは、ShoppingListはReact Component クラス、またはReact Componentタイプです。Componetはprops
(「プロパティ」の略)と呼ばれるパラメータを受け取り、render
メソッドを介して表示するビューの階層を返します。
renderメソッドは、画面に表示したい内容のdescription
を返します。 Reactはdescription
を受け取り、結果を表示します。 特に、renderはReact element
を返し 、これは何を描画するかについての簡単な説明です。ほとんどのReact開発者は“JSX”
と呼ばれる特別な構文を使います。
return React.createElement('div', {className: 'shopping-list'},
React.createElement('h1', /* ... h1 children ... */),
React.createElement('ul', /* ... ul children ... */)
);
Inspecting the Starter Code
ローカル環境のセットアップで作成した、コードを元にReactを学びます。この章を通して、以下の3つのReact componentが使用されていることがわかります。
- Square
- Board
- Game
Squareコンポーネントは単一の<button>をレンダリングし、Boardは9つの正方形をレンダリングします。 Gameコンポーネントは、後で変更するプレースホルダ値を使ってボードをレンダリングします。 現在対話型コンポーネントはありません。
Passing Data Through Props
ではまず、手始めにBoard componentからSquare componentにデータを渡してみましょう。
BoardのrenderSquare
メソッドを編集して、value
というpropをSquereへ渡しましょう。
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />;
}
Squareのrender
メソッドの{/* TODO */}
を{this.props.value}
で置き換えて、value
を表示してみましょう。
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value}
</button>
);
}
}
ブラウザを更新すると、以下のように表示されるはずです。
Making an Interactive Component
四角をクリックしたときに、"X"を表示される処理を書いてみましょう。まず、Square componentのrenderメソッドから返されるbuttonタグを次のように変更します。
class Square extends React.Component {
render() {
return (
<button className="square" onClick={() => alert('click')}>
{this.props.value}
</button>
);
}
}
この状態で、四角をクリックすると、ブラウザにアラートが表示されるはずです。
次のステップとして、Square componentにクリックされたことを「記憶」させ、それに"X"マークを書き込みます。物事を「記憶」するために、componentsはstate
を使用します。
Reactコンポーネントは、コンストラクター内でthis.state
を設定することによってstate
を持つことができます。 this.state
は、それが定義されているReact componentに対してプライベートであると見なされるべきです。Squareの現在の値をthis.stateに格納し、Squareがクリックされたときにそれを変更しましょう。
まず、状態を初期化するためにクラスにコンストラクタを追加します。
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
<button className="square" onClick={() => alert('click')}>
{this.props.value}
</button>
);
}
}
Note
JavaScriptクラスでは、サブクラスのコンストラクタを定義するときには常にsuperを呼び出す必要があります。コンストラクターを持つすべてのReactコンポーネントクラスは、super(props)呼び出しで開始する必要があります。
クリックしたときに現在の状態の値を表示するように、Squareのレンダリング方法を変更します。
- <button>タグ内でthis.props.valueをthis.state.valueに置き換えます。
-
()=> alert()
イベントハンドラを()=> this.setState({value: 'X'})
に置き換えます。 - 読みやすくするため、
className
とonClick
を別の行に分けます。
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
<button
className="square"
onClick={() => this.setState({value: 'X'})}
>
{this.state.value}
</button>
);
}
}
SquareのrenderメソッドでonClickハンドラからthis.setState
を呼び出すことで、<button>がクリックされるたびにそのSquareを再描画するようにReactに指示します。 更新後、this.state.value
は「X」になりますので、ゲームボードにXが表示されます。 正方形をクリックすると、Xが表示されます。
コンポーネント内でsetState
を呼び出すと、Reactはその内部にある子コンポーネントも自動的に更新します。
Completing the Game
これで、三目並べゲームの基本的な構成要素が完成しました。 ゲームを完成させるためには、今度はボード上に「X」と「O」を交互に配置する必要があり、勝者を決定する方法が必要です。
Lifting State Up
現在、各Square componentはゲームの状態を管理しています。 勝者を確認するには、9つの各正方形の値を1か所にまとめる必要があります。
Boardが各Squareに状態を尋ねればよいと考えるかもしれませんが、コードが複雑になり、バグを生む原因になります。その代わりに、ゲームの状態を各Squareではなく、親のBoard componentに保存します。Boardコンポーネントは、propsを使用して、表示する内容を各Squireに渡すことができます。
複数の子コンポーネントからデータを収集する、または2つの子コンポーネントが互いに通信するようにするには、代わりにそれらの親コンポーネントで共有状態を宣言する必要があります。 親コンポーネントは、propsを使用して状態を子に渡すことができます。 これにより、子コンポーネントは互いに、また親コンポーネントと同期します。
Reactコンポーネントがリファクタリングされるとき、親コンポーネントに状態を持ち上げることは一般的です。Boardにコンストラクタを追加し、Boardの初期状態を9個のnullを含む配列に設定します。 これら9個のnullは9個の正方形に対応します。
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
renderSquare(i) {
return <Square value={i} />;
}
render() {
const status = 'Next player: X';
return (
<div>
<div className="status">{status}</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>
);
}
}
Boardの現在の値( 'X'、 'O'、またはnull)について各Squareに指示するようにBoardを変更します。 Boardのコンストラクタでsquares配列を定義したので、それを読み込むためにBoardのrenderSquareメソッドを変更します。
renderSquare(i) {
return <Square value={this.state.squares[i]} />;
}
各Squareは、 'X'、 'O'、または空のSquareの場合はnullになる値を受け取るようになります。
次に、正方形をクリックしたときの動作を変更する必要があります。Board componentはどの正方形が塗りつぶされているかを管理します。そのため、正方形がBoardの状態を更新する方法を作成する必要があります。しかし、state
はコンポーネントに対して、privateであるため、正方形から直接Boardのstate
を更新することはできません。
Boardのstateのプライバシーを維持するために、BoardからSquareにメソッドを委任します。 この関数は、Squareがクリックされたときに呼び出されます。 BoardのrenderSquareメソッドを次のように変更します。
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
これで、BoardからSquareに、valueとonClickの2つのpropsを渡しています。onClick propは、クリックしたときにSquareが呼び出すことができる関数です。Squareに次の変更を加えます。
- Squareのrenderメソッドでthis.state.valueをthis.props.valueに置き換えます。
- Squareのrenderメソッドでthis.setState()をthis.props.onClick()に置き換えます
- Squareはゲームの状態を追跡しなくなったため、Squareからコンストラクタを削除します。
class Square extends React.Component {
render() {
return (
<button
className="square"
onClick={() => this.props.onClick()}
>
{this.props.value}
</button>
);
}
}
Squareがクリックされると、Boardが提供するonClick関数が呼び出されます。この関数は以下のように動作します。
- 組み込みDOM <button>コンポーネントのonClick propは、クリックイベントリスナを設定するようにReactに指示します。
- ボタンがクリックされると、ReactはSquareのrender()メソッドで定義されているonClickイベントハンドラを呼び出します。
- このイベントハンドラはthis.props.onClick()を呼び出します。
- Boardは
onClick = {()=> this.handleClick(i)}
をSquareに渡したため、クリックされるとSquareはthis.handleClick(i)
を呼び出します。 - handleClick()メソッドをまだ定義していないので、コードがクラッシュします。
Note
DOM <button>要素のonClick属性は組み込みコンポーネントなので、Reactにとって特別な意味を持ちます。 Squareのようなカスタムコンポーネントの場合、命名はあなた次第です。 SquareのonClickプロップまたはBoardのhandleClickメソッドに別の名前を付けることができます。ただ、Raactではイベントを表すprop名には、on[Event]
を使い、イベントを扱うメソッドには、handle[Event]
を使うのが慣例です。
次に、handleClic
をBoardクラスに追加します。
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = 'X';
this.setState({squares: squares});
}
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
const status = 'Next player: X';
return (
<div>
<div className="status">{status}</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>
);
}
}
これらの変更が終わったら、Squareをクリックして塗りつぶすことができます。ただし、現在、状態は個々のSquareコンポーネントではなくBoardコンポーネントに格納されています。Boardの状態が変わると、Squareコンポーネントは自動的に再レンダリングされます。Boardコンポーネントのすべてのマスの状態を維持することで、将来的に勝者を決定することができます。
Squareコンポーネントは状態を維持しなくなったため、SquareコンポーネントはBoardコンポーネントから値を受け取り、クリックされるとBoardコンポーネントに通知します。Reactの用語では、Squareコンポーネントは現在コントロールされているコンポーネントです。 Boardはそれらを完全に管理しています。
handleClickで、既存の配列を変更するのではなく、変更する正方配列のコピーを作成するために.slice()を呼び出す方法に注意してください。 次のセクションで、なぜsquares配列のコピーを作成するのかを説明します。
Why Immutability Is Important
前のコード例では、既存の配列を変更するのではなく、.slice()演算子を使用して、変更する正方配列のコピーを作成することをお勧めしました。 不変性について、そしてなぜ不変性を学ぶことが重要なのかを説明します。
データを変更するには、一般的に2つの方法があります。 最初の方法は、データの値を直接変更してデータを変更することです。2番目の方法は、データを、必要な変更を加えた新しいコピーと置き換えることです。
Complex Features Become Simple
不変性により、複雑な機能の実装がはるかに簡単になります。 このチュートリアルの後半では、三目並べゲームの履歴を確認して前の動きに「戻る」ことができる「タイムトラベル」機能を実装します。 この機能はゲームに固有のものではありません。特定の操作を元に戻したりやり直したりする機能は、アプリケーションでは一般的な要件です。 直接のデータ変更を回避することで、以前のバージョンのゲームの履歴をそのまま維持し、後でそれらを再利用することができます。
Detecting Changes
可変オブジェクトは直接変更されるため、変更可能オブジェクトの変更を検出するのは困難です。 この検出では、可変オブジェクトをそれ自体の以前のコピーと比較し、オブジェクトツリー全体を確認する必要があります。
不変オブジェクトの変化を検出するのはかなり簡単です。 参照されている不変オブジェクトが前のものと異なる場合は、オブジェクトは変更されています。
Determining When to Re-render in React
不変性の主な利点は、Reactで純粋なコンポーネントを構築するのに役立つということです。 不変データは、変更が行われたかどうかを容易に判断することができ、コンポーネントがいつ再レンダリングを必要とするかを判断するのに役立ちます。
Function Components
今度はSquareを関数コンポーネントに変更します。
Reactでは、関数コンポーネントはレンダリングメソッドのみを含み、独自の状態を持たないコンポーネントを書くためのより簡単な方法です。React.Componentを拡張するクラスを定義する代わりに、propsを入力として受け取り、レンダリングされるべきものを返す関数を書くことができます。関数コンポーネントはクラスよりも書くのが面倒ではなく、多くのコンポーネントはこのように表現できます。
Squareクラスをこの関数に置き換えます。
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
Taking Turns
現状では、ボード上に'O'をマークすることができません。
デフォルトでは最初の移動を「X」に設定します。 Boardコンストラクタの初期状態を変更することでこのデフォルトを設定できます。
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
プレーヤーが移動するたびに、xIsNext(ブール値)が反転して、どのプレーヤーが次に進むかが決定され、ゲームの状態が保存されます。 xIsNextの値を反転するようにBoardのhandleClick関数を更新します。
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
この変更により、「X」と「O」は交代することができます。 次のターンのプレイヤーが表示されるように、Boardのレンダリングの「ステータス」テキストも変更しましょう。
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
// the rest has not changed
Declaring a Winner
次に、どのプレイヤーがゲームに勝ったのかを示す必要があります。ファイルの最後にこのヘルパー関数を追加することで勝者を決めることができます。
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],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
プレーヤーが勝ったかどうかを確認するために、Boardのrender関数でcalculateWinner(square)を呼び出します。 プレイヤーが勝った場合は、「Winner:X」や「Winner:O」などのテキストを表示できます。Boardのレンダリング機能のステータス宣言を次のコードに置き換えます。
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
// the rest has not changed
誰かがゲームに勝った場合、またはSquareがすでにいっぱいになった場合はクリックを無視することで、BoardのhandleClick関数を早く戻るように変更できます。
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
おめでとうございます。三目並べゲームは以上で完成です。
Adding Time Travel
演習の最後として、「戻る」機能の実装を行います。
Storing a History of Moves
平方配列を変更した場合、タイムトラベルの実装は非常に困難になります。
しかし、私達はslice()を使って移動のたびにsquares配列の新しいコピーを作成し、それを不偏変数として扱いました。これにより、過去のバージョンのsquares配列をすべて保存し、ターン間を移動できます。
過去の正方形の配列をhistory
と呼ばれる別の配列に格納します。history配列は最初の移動から最後の移動までのすべてのボードの状態を表し、次のような形をしています。
history = [
// Before first move
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// After first move
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// After second move
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
今度は、どのコンポーネントが履歴状態を所有するべきかを決定する必要があります。
Lifting State Up, Again
最上位のGameコンポーネントに過去の移動のリストを表示させます。 そのためには履歴にアクセスする必要があるので、履歴の状態をトップレベルのGameコンポーネントに配置します。
履歴状態をGameコンポーネントに配置することで、その子Boardコンポーネントからsquares状態を削除できます。 SquareコンポーネントからBoardコンポーネントにステートを「持ち上げた」ように、今度はそれをBoardからトップレベルのGameコンポーネントに持ち上げます。 これにより、ゲームコンポーネントはボードのデータを完全に管理できるようになり、歴史からの過去のターンをレンダリングするようボードに指示することができます。
まず、コンストラクタ内でGameコンポーネントの初期状態を設定します。
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
次に、BoardコンポーネントにGameコンポーネントからsquares
とonClick
propsを渡します。Boardには多くのSquareのシングルクリックハンドラがあるので、どのSquareがクリックされたのかを示すために、各Squareの位置をonClickハンドラに渡す必要があります。Boardコンポーネントを変換するために必要な手順は次のとおりです。
- Boardのコンストラクタを削除します。
- BoardのrenderSquareで
this.state.squares[i]
をthis.props.squares[i]
に置き換えます - BoardのrenderSquareで
this.handleClick(i)
をthis.props.onClick(i)
に置き換えます。
class Board extends React.Component {
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
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>
<div className="status">{status}</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>
);
}
}
Game componentのレンダリング機能を更新して、最新の履歴エントリを使用してゲームのステータスを判断して表示します。
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
Gameコンポーネントがゲームのステータスをレンダリングしているので、Boardのrenderメソッドから対応するコードを削除することができます。
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>
);
}
最後に、handleClickメソッドをBoardコンポーネントからGameコンポーネントに移動する必要があります。 Gameコンポーネントの状態は異なる構造になっているため、handleClickも変更する必要があります。
handleClick(i) {
const history = this.state.history;
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
}]),
xIsNext: !this.state.xIsNext,
});
}
Showing the Past Moves
三目並べゲームの歴史を記録しているので、過去の履歴をプレイヤーに表示することができます。
mapメソッドを使用して、移動履歴を画面上のボタンを表示します。
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
Implementing Time Travel
三目並べゲームの履歴では、それぞれの過去の動きはそれに関連したユニークなIDを持っています。それは履歴の連続した番号です。 履歴が並べ替えられたり、削除されたり、途中で挿入されたりすることはありません。そのため、履歴のインデックスをキーとして使用しても安全です。
Gameコンポーネントのrenderメソッドで、キーを<li key = {move}>
として追加できます。キーに関するReactの警告は消えます。
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>
);
});
jumpToメソッドは未定義であるため、リスト項目のボタンをクリックするとエラーが発生します。 jumpToを実装する前に、ゲームコンポーネントの状態にstepNumberを追加して、現在表示しているステップを示します。
まず、Gameのコンストラクタの初期状態にstepNumber:0を追加します。
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
xIsNext: true,
};
}
次に、そのstepNumberを更新するために、GameでjumpToメソッドを定義します。 また、stepNumberを変更する数が偶数の場合、xIsNextをtrueに設定します。
handleClick(i) {
// this method has not changed
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0,
});
}
render() {
// this method has not changed
}
Squareをクリックしたときに発生する、GameのhandleClickメソッドにいくつか変更を加えます。
追加したstepNumberの状態は、現在ユーザーに表示されている動きを反映しています。 新しい動きをした後、this.setState引数の一部としてstepNumber:history.lengthを追加してstepNumberを更新する必要があります。これにより、新しい動きがあった後も同じ動きを見せつけてしまうことがなくなります。
this.state.historyの読み取りもthis.state.history.slice(0、this.state.stepNumber + 1)に置き換えます。 これにより、「過去にさかのぼって」その時点から新たな動きをした場合に、誤ったものになる可能性のある「将来の」履歴をすべて破棄することができます。
handleClick(i) {
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) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
});
}
最後に、stepNumberに従って、Gameコンポーネントのrenderメソッドを、常に最後の履歴をレンダリングするものから現在選択されているものをレンダリングするように変更します。
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
以上で、チュートリアル終了です。
所感
- WEB界隈は技術の進歩が早い
- 3年前くらいに大学で構築してたときは、RailsとJQueryとかが主流だったが、いつの間にかnode.js、React、Vueが流行ってついていけません。。。
- 今どきのフロントエンドフレームワークは、覚えるとやっぱり便利。ただ、処理系もある程度フロントエンドで各必要があるので、今後はフロントエンドのプログラマもバックエンドっぽい処理を書ける必要がありそう。
- 次は、Reactだけでなく、CSSフレームワークやAPI連携部分など勉強したい。