42. Tutorial: Intro to React
【所要時間】
7時間52分(2018年8月23,24日)
【概要】
Reactのチュートリアル
【要約・学んだこと】
What Is React?
Reactはuser interfaceを構築する宣言的で、効率的で、柔軟性のある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" />
スクリーン上で何が見たいかをReactに伝えるためにcomponentsを使う。データが変わると、Reactは効率的にcomponentsを更新し、re-renderする。
ShoppinglistはReact component class、またはReact component typeだ。componentはprops(propertiesの略)と呼ばれるparameterを受け取り、render メソッドを介して表示するためのビューの階層を返す。
renderメソッドはスクリーンで見たいことのdescriptionをreturnする。Reactはdescriptionを受け取り、結果を表示する。特に、renderはReact elementをreturnする。これは何をrenderするかの軽い記述だ。
ほとんどのReact ディベロッパーはJSXと呼ばれる特別なsyntaxを使い、記述構造を簡単にする。<div /> syntaxはビルドタイムでReact.createElement(‘div’)に変換される。上記の例は次のものと同じ。
return React.createElement('div', {className: 'shopping-list'},
React.createElement('h1', /* ... h1 children ... */),
React.createElement('ul', /* ... ul children ... */)
);
ここではcreateElement()は使わず、代わりにJSXを使い続ける。
JSXはJSの機能が満載だ。JSX内の中括弧内に、どんなJS expressionでも入れることができる。それぞれのReact elementは、variableに格納するか、プログラム内で渡すことができるJSオブジェクトだ。
上記のShoppinglist compotentは<div />と <li />のようなビルトインDOM componentのレンダーのみを行う。しかし、カスタムReact componentを構築し、レンダーすることも可能。例えば、<ShoppingList />と書くことでショッピングリスト全体を参照することができる。それぞれのReact componentはカプセル化され、独立して動作することができる。このため、単純なcomponentsから複雑なUIを構築することができる。
Inspecting the Starter Code
The Starter Codeには
- Square
- Board
- Game
3つのReact componentがある。
Square componentは1つの<button>をrenderする。
Boardは9つのsquareをrenderする。
Game componentは後に変更するプレースホルダーvalueをもつboardをrenderする。
現在componentの相互作用はない。
Passing Data Through Props
Board componentからSquare componentにいくつかデータを送ってみる。
BoardのrenderSquareメソッドをvalueと呼ばれるpropをSquareにパスするコードに変える。
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />;
}
valueを表示するSquareのrenderメソッドを{/ * TODO * /}を{this.props.value}に置き換えることで変更する。
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value}
</button>
);
}
}
それぞれのsquareにrenderでアウトプットされた数字が表示される。
親であるBoard componentから子のSquare componentにpropをパスした。
Making an Interactive Component
Squareをクリックするとブラウザにアラートが表示されるようにする。
class Square extends React.Component {
render() {
return (
<button className=”square” onClick={function() { alert(“click”)}}>
{this.props.value}
</button>
);
}
}
次に、Square componentがクリックされたことを記憶し、”X”マークを埋めるようにする。記憶させるためにはcomponentがstateを使う。
React componentはconstructorにthis.stateをセットすることで、stateを持つことができる。
this.stateはそれが定義されているReact componentのプライベートとして見なされる。this.stateにSquareの現在のvalueを格納し、Squareがクリックされた時に変更する。
まず、classにコンストラクターを追加し、stateを初期化する。
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
JSでは、サブクラスにconstructorを定義する時は、必ずsuperのコールが必要。constructorを持つ全てのReact component classは、super(props)で始まる。
次にSquareのrender メソッドがクリックされた時点のstateのvalueを表示するように変更する。
- <button>タグ内のthis.props.valueをthis.state.valueに変える
- () => alert() イベントハンドラーを () => this.setState({value: “X”})に変える。
- classNameとonClick propsを読みやすいように別の行に置く。
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のレンダーメソッドのonClick ハンドラーからthis.setStateを呼ぶことで、Reactに<button>がクリックされるたびにre-renderするように指示する。更新後、Squareのthis.state.valueは”X”となるので、game boardでXをみることができる。SquareのどこをクリックしてもXが現れる。
setStateをcomponentでコールする時、Reactは自動で子のcomponentの中身も更新する。
Developer Tools
Chromeの拡張機能としてダウンロードできた。
Completing the Game
現在tic-tac-toeゲームの基本的なブロックが構築出来た。ゲームを完成させるために、”X”と”O”をボードに配置し、価値を決める方法が必要だ。
Lifting State Up
現在それぞれのSquare componentはgameのstateを維持している。勝者を確かめるために、1箇所に9つのsquareそれぞれのvalueを維持する。
BoardがそれぞれのSquareにそれぞれの情報を尋ねることもできるが、コードの理解が難しくなる。それぞれのSquareの代わりに親のBoard componentのgameの状態を蓄えるアプローチをする。Board componentはそれぞれのSquareに数字を渡した時のように、propを渡すことで、それぞれのSquareが何を表示するか伝えることができる。
複数の子からデータを集める、もしくは2つの子 componentsがお互いに影響を与えるために、親 componentが代わりにshared stateを宣言する必要がある。親 componentはpropsを使うことで子にstateを戻すことができる。つまりこれは子 componentをお互い、また親component と同期させて維持する。
親 componentにlifting stateするのは、React componentがリファクタされる際にはよくある。
Boardにconstructorを追加し、Boardのinitial stateを9つのnullを持つarrayにセットする。9つのnullは9つのsquareに対応する。
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の renderSquareメソッドを変更する。
最初の状態では、すべてのSquareで0~8の数字を表示するため、Boardからのprop downのvalueを渡した。次はSquareのstateによって”X”マークが決定され、numberが配置された。そのため、現在Squareはvalue propがBoardによって渡されるものを無視している。
ここでprop passing mechanismを再び使う。BoardをそれぞれのSquareにそれぞれのSquareの現在のValue(“X”, “O”, null)を指示するように修正する。すでにBoardのconstructorにsquares arrayを宣言している。そしてBoardの renderSquareメソッドをそれから読むために修正する。
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
}
}
renderSquare(i) {
return <Square value={this.state.squares[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がvalue propである”X”, “O”, nullのいずれかを受け取るようになった。
次はSquareがクリックされた時に何が起こるかを変更する必要がある。Board componentは現在どのsquaresが埋められているかを維持する。SquareがBoardのstateを更新する方法を作る必要がある。
stateはそれを定義するためのcomponentにとってプライベートだと見なされるので、Squareから直接Boardのstateを直接更新出来ない。
Boardのstateのプライバシーを維持するには、BoardからSquareにfunctionを渡す。このfunctionはSquareがクリックされた時にコールを取得する。
BoardのrenderSquareを変更しよう。
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
}
}
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>
);
}
}
今BoardからSquareに2つのpropsを渡した。valueとonClickだ。
onClick propはSquareがクリックされたときに呼ぶことができるfunctionだ。Squareの変更点は
- Squareのrenderメソッドのthis.state.valueをthis.props.valueに置き換える。
- Squareのrenderメソッドのthis.setState()をthis.props.onClick()に起きかる。
- Squareはもうgameのstateの記録を追う必要がないので、Squareのconstructorを消去。
class Square extends React.Component {
render() {
return (
<button
className="square"
onClick={() => this.props.onClick()}
>
{this.props.value}
</button>
);
}
}
これでSquareがクリックされると、onClick functionがBoardに呼ばれることで提供される。ここでの動作は
- built-in Dom<button> componentのonClick propは、Reactにクリックイベントリスナーのセットアップを指示する。
- buttonがクリックされると、ReactはSquareのrender() メソッドで定義されたonClickイベントのハンドラーを呼ぶ。
- イベントハンドラーはthis.props.onClick() を呼ぶ。SquareのonClick propはBoardに指定される。
- BoardがSquareに onClick={() => this.handleClick(i)} を渡したので、Squareはクリックさっれるとthis.handleClick(i)を呼ぶ。
- handleClick()をまだ定義していないので、コードがクラッシュする。
Note
DOM<button>elementのonClick attributeは、Built-in componentのため、Reactに特別な意味を持つ。Squareのようなcustom componentの場合は、名前は自分で決められる。SquareのonClick propまたはBoardのhandleClickメソッドに別の名前をつけられる。しかしReactでは、イベントを表現するpropsには on[Event] namesを使い、イベントを扱うメソッドには handle [Event] を使うのが一般的だ。
ここでまだ定義されていないhandleClickをBoard classに追加する。
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>
);
}
}
このチェンジで再びSquaresをクリックして埋めることができるようになった。しかし、今はstateがそれぞれのSquare componentの代わりに、Board componentに格納されている。Boardのstateが変わる時、Square componentはre-renderを自動的にする。
Board componentの全てのsquaresのstateをキープすることが、のちに勝者を決めるのに必要。
Square component がこれ以上stateを維持しないので、Square componentはBoard componentからvalueを受け取り、clickされた時にBoard componentに情報を与える。
Reactの用語では、Square componentは現在controlled componentsである。Boardはそれらを完全にコントロールしている。
handleClickでは、既存のarrayを修正する代わりに、squaresのarrayのコピーを作るために .slice() をコールする。
Why Immutability Is Important
通常データを変えるアプローチは2つある。データのvalueを直接変える可変データと、望んだ変更をもつ新しいコピーとデータを置き換える方法だ。
結果は同じだが、直接可変しないほうが幾らかのメリットがある。
- 普遍性は複雑な機能を簡単に実行する。
- 直接変更されるため、可変オブジェクトの変更は検出がむずかしいが、不変(immutability)なら簡単。
- immutabilityの主なメリットは、Reactでpure componentをbuiltするのに役立つ。不変データはcomponentがre-renderingを要求する時を判断するのに役立つ変更が行われたかどうかを簡単に判断できる。
Functional Components
Squareをfunctional componentに変更する。
Reactでは、functional componentsはrenderメソッドだけを含み、独自のstateを持たないcomponentを書くための簡単な方法だ。React.Componentを拡張するクラスを定義する代わりに、inputとしてpropsを受け取り、何をrenderするべきかをreturnするfunctionを書くことができる。
Note
Squareをfunctional componentに修正する時、onClick={() => this.props.onClick()}
を onClick={props.onClick}
に変える。クラスではarrow functionを正しいthis valueにアクセスするために使うが、functional componentではthisについて心配する必要がない。
Taking Turns
現時点では“O”がボードにマークされないので、それを修正する。
デフォルトで最初の動きが”X”になるように設定する。Board constructorのinitial stateを修正することで、これをデフォルトにセットできる。
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
}
}
プレーヤーが動くごとに、xIsNext(boolean)は反転し、次のプレーヤーを決定し、ゲームの state を保存する。
BoardのhandleClick functionを更新し、xIsNextのvalueを反転する。
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? "X" : "O";
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
Boardのrenderの”status”テキストを変更し、次のプレーヤーがどちらになるかを表示する。
render() {
const status =
'Next player: ' + (this.state.xIsNext ? "X" : "O");
Declaring a Winner
次のプレーヤーがどっちになるかを表示するようになったので、次はゲームに勝ったときと、これ以上のターンがない時を表示する。helper functionをファイルの最後に加えることで、勝者を決めることができる。
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 functionでcalculateWinner(squares)をコールし、プレーヤーが勝ったかどうかを確認する。もしプレーヤーが勝ったら、テキストを表示できる。Boardのstatus declarationをrender functionに置き換える。
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}
BoardのhandleClick functionを、誰かが勝つか、Squareが埋まった場合に、クリックを無視して早期に戻るようにする。
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
squares arrayがmutatedだと、time travel機能を追加するのはとても難しい。
しかし、ここではslice()を全ての動きの後にsquares arrayの新しいコピーを作成するのに使っており、immutableとして扱っている。
historyと呼ばれる他のarrayに過去のsquares arrayを保存する。history arrayは全てのboard statesを表す。
どのcomponentがhistory stateを持つかを決めなければいけない。
Lifting State Up, Again
top-level Game componentに過去の動きのリストを表示したい。それは動作するためにhistoryにアクセスする必要があるので、top-level game componentにhistory stateを置く。
Game componentにhistory stateを置くことは、子のBoard componentのsquares stateを消去していいということだ。
Boardからtop-level Game componentにlift upする。これはGame componentがBoardのデータをフルコントロールし、Boardにhistoryから過去のターンをrenderするように指示する。
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 componentがsquaresとonClick propsをGame componentから受け取るようにする。BoardにたくさんのSquaresのためのシングルクリックハンドラーを持っているので、それぞれのSquareの場所をonClick handlerがどのSquareがクリックされたかを示すようにわたさなければいけない。Board componentは下記の点を変える。
- Boardのconstructorを消去する。
- 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>
);
}
}
最新のhistory entryを使用して、ゲームのstatusを表示するために、Game componentのrender functionを更新する。
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 componentは現在gameのstatesをrenderingしているので、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>
);
}
}
Board componentからGame componentに handleClick メソッドを移動する必要がある。また、Game componentのstateは異なる構成をしているので、handleClickを修正する必要がある。GameのhandleClickメソッド内で、新しいhistoryのentryをhistoryに連結する。
handleClick(i) {
const history = this.state.history;
const current = history[history.length - 1];
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? "X" : "O";
this.setState({
history: history.contact([{
squares: squares,
}])
xIsNext: !this.state.xIsNext,
});
}
Note
array push()メソッドと異なり、contact() メソッドはオリジナルのarrayを可変させないの。
Board componentはrenderSquareとrenderメソッドだけを必要としている。gameの state と handleClickメソッドはGame componentの中にあるべきだ。
Showing the Past Moves
ゲームのhistoryを記録したので、過去の動きを表示させる。
Reactで複数のアイテムをrenderするために、React elementのarrayを使うことができる。
JSでは、arrayはmapping dateを他のdataに送るのによく使うmap()メソッドを持つ。
例
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]
mapメソッドを使い、動きの記録を画面上のbuttonを表すReact elementにマップし、過去の移動にjumpするボタンのリストを表示できる。
Gameのrenderメソッドのhistoryを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>
);
}
}
tic-toc-toeのgameのhistoryが動くたびに、<button>を含むlist item<li>を作る。buttonはthis.jumpTo()メソッドを呼ぶonClickハンドラーを持つ。
まだjumpTo()メソッドは実行していない。
今の所ゲームで発生した動きの一覧と、警告が表示される。
Picking a Key
listをrenderするとき、Reactはrenderされたそれぞれのリストに関する情報を保存する。リストを更新すると、Reactは何が変わったかを決める必要がある。リストの追加、削除、並べ替え、更新ができる。
Reactは我々の意図が読めないので、各リストアイテムをその兄弟と区別するために、各リストアイテムのkey propertyを指定する必要がある。
elementが作られると、Reactはkey propertyを抽出し、keyを直接elementにreturnする。keyはpropsに属しているように見えるが、keyはthis.props.keyを使って参照することはできない。Reactは自動的にkeyを使用して、更新するcomponentを決める。componentはkeyについて尋ねることができない。
リストがre-renderされるとき、Reactはそれぞれのリストのアイテムのkeyを受け取り、keyにマッチする以前のリストのアイテムを探す。もし現在のlistが以前のリストに存在しないkeyを持つなら、Reactはcomponentを作る。現在のリストが以前のリストに存在していたkeyを見逃したなら、Reactはcomponentを破壊する。keyはReactに、Reactがre-renderする間の状態を維持することを可能にする各componentのアイデンティティについて、Reactに伝える。もしcomponentのkeyが変われば、componentは破壊され、新しいstateが再び作られる。
dynamic listをbuildするときは、必ず適切なkeyを割り当てることを強く勧める。そうでなければ、自分でデータを再構築することを考えるべきだ。
もしkeyが指定されなければ、Reactは警告を表現し、デフォルトのkeyとして、arrayインデックスを使う。keyとしてarrayインデックスを使うのはリストの項目の並べ替えや挿入/削除を試みる時に問題となる。明示的にkey={i}を渡すと警告は消えるが、arrayと同じ問題があり、ほとんどの場合は推奨されない。
keyはグローバルに独自である必要はない。componentとその兄弟の間で同一である必要がある。
Implementing Time Travel
このゲームのhistoryは、それぞれの過去の移動が独自のIDを持つ。それは移動の連続番号だ。移動は決して並び替えられず、削除されず、間に挿入されることもないので、keyとして移動インデックスを使うのは安全だ。
Game componentのrenderメソッドに、<li key={move}>としてkeyを加え、Reactのkeyについての警告が消える。
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 key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
リストアイテムのbuttonをクリックすると、jumpToメソッドが定義さていないので、エラーを投げる。
jumpToを実行する前に、stepNumberをGame componentのstateにどのstepを現在見ているのか指示する。
まずはGameのconstructorに最初のstateのためにstepNumber: 0を加える。
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null)
}],
stepNumber: 0,
xIsNext: true
};
}
次にGameのjumpToメソッドをstepNumberを更新するために定義する。また、もしstepNumberで変えた数字が偶数なら、xIsNextをtrueにする。
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,
});
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0
});
}render() {
squareをクリックした時のGameのhandleClickメソッドの動作を少し変更する。
加えたstepNumber stateは、ユーザーに表示された移動を反映する。新しい移動の後、this.setState argumentの一部として、stepNumber: history.lengthを加えることで、stepNumberを更新する必要がある。これは、新しいものが作成された後も同じ動きを示す。
また、this.state.historyをthis.state.history.slice(0, this.state.stepNumber + 1)に置き換える。これはもし時を遡りをし、そのポイントから新しい移動をするなら、未来のhistoryが全て間違いであることを確かにする。
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,
});
}
最後に、Game componentのrenderメソッドを、常に最後のrenderからrenderingし、stepNumberにしたがって現在選択されている移動をrenderするように修正する。
render() {
const history = this.state.history;
const current = history[this.history.stepNumber];
const winner = calculateWinner(current.squares);
gameのhistoryをどこのステップでクリックしても、tic-tac-toe boardはすぐにstepが発生した後の見た目になる。
完成。
【わからなかったこと】
【感想】
とりあえず手順に沿って作ってみたら全然わからなかったが、特に理解の助けにならなかったので無駄だった。
チュートリアルを1つ1つやっても簡単ではなかった。