業務で開発しているプロジェクトでReact
を入れることになりまして、そのためのインプットとしてReactの公式チュートリアルを行いました。
今回はチュートリアルで学んだことを整理しつつ1つ1つの工程ごとにまとめていこうと思います。
Reactの知識が何もない初心者の方でもわかってもらえるように、押さえておくべき部分が出てきた時にその都度解説を挟むようにしています。
Contents
Reactチュートリアルとは
そもそもReact
はFacebookが開発したもので、公式ページにチュートリアルがあります。こちらにある内容と同じものを今回作成していきました。英語で書かれていますが、こちらでは日本語でまとめていこうと思います。
開発環境
React
の環境はすごく簡単に作成することができて、やり方に関してはこちらの記事を参考にしてください。
今回みたいにチュートリアルだけ手軽にやってみたい、というような場合には環境構築をしなくてもhtml
ファイルに直接書いていくのでも大丈夫です。
その場合は、
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
//ここから
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/[email protected]/babel.min.js"></script>
//ここまでを追加
</head>
<body>
<div id="container"></div>
<script type="text/babel"> // text/babelにする
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value}
</button>
);
}
}
︙
</script>
</body>
</html>
このようにして書いていけば、ブラウザ上でちゃんと表示されると思います。
ファイル分割をしようと思うとうまくいかないので、分割してしっかり書きたい場合は開発環境を整えていきましょう。
作成するもの
作成するのはこのようなものです。
誰でもやってことがある「○×ゲーム」です。公式ページではtic-tac-toeゲーム(三目並べ)と呼ばれています。
機能としては
- プレイヤーが交互に順番が回ってくる
- すでに埋まったマスは埋めれない
- 1列揃った時点で終了
- 上に現状のプレイヤーが誰か、勝負がついた場合に勝者が通知される
- 右に履歴がでてきてボタンを押したらその時点に遡ることができる。
というようなものを作成していきます。
本家の完成版がこちらにのっています。
https://github.com/fkt1993/react-sample-app
今回作成したものはこちらのリポジトリにまとめましたのでそちらでも確認ができます。
初期設定
ある程度出来上がったコードが用意してあって、それをいじっていくことになります。
HTML
,css
の部分は完成されているのでjavascript
の部分をひたすらいじっていきます。
本家の最初のコードはこちらです。
https://github.com/fkt1993/react-sample-appでも、コミットを遡れば最初の状態のものがあるので参照することができます。
これ以降の手順を1つずつコミットしてあるので段階ごとに遡れるようにしています。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
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('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],
];
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;
}
このようになっています。
簡単に説明していきます。
まずhtml
でcontainer
というid
のdiv要素
があるのでそこに対して、
ReactDOM.render(
<Game />,
document.getElementById('container')
);
こう書くことによってcontainer
にGameコンポーネント
でレンダリングされたDOM
が入ります。
コンポーネントというのはクラスで書かれていて、renderメソッド
を定義すれば先程のというように書けばこの部分にrenderメソッド
で返されるDOM
がレンダリングします。
あとで説明しますが、state
として値を保持することもできます。
次にGameコンポーネント
を見ていきましょう。
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>
);
}
}
先ほど説明したように、<Game />
の部分にはrenderメソッド
内に書かれているDOM
が返されます。
よく見てみるとこの中身は見慣れたものかと思いきや少し違う部分があるかと思います。
これはjsx
といってこれもfacebook
が開発した独自タグの技術です。
普段DOM
をjavascript
で定義するとき文字列の連結を駆使してやっているとわりと見にくかったりします。jsx
の表記だとhtml
に近い書き方なので見やすいですがいくつかお作法があります。
その1つがclassName
です。class
がjavascript
の予約後なのでhtml
のクラスを定義する際にjsx
の中ではclassName
と記述します。
もうひとつ、変数を埋め込む際には{}
で囲むだけで大丈夫です。これによって変数を埋め込む場合でも見やすいです。{/* status */}
とあるのは{}
の中身はコメントなので後々書き換えて表示するようにしていく部分です。
Gameコンポーネント
のrenderメソッド
内にも<Board />
というのがあるのでBoardコンポーネント
をみていきます。
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>
);
}
}
Boardコンポーネント
はこの様になっています。ここでは3×3
のマスをレンダリングしています。
renderメソッド
内を見ていくと、
{this.renderSquare(0)}
というようになっています。
先程も説明しましたがjsx
で、変数を埋め込む場合の表記は{}
で囲むだけです。
ここでは、renderメソッド
内でstatus
を定義してそれを{status}
と書いて埋め込んでいます。
また、Boardコンポーネント
内にrenderSquare
というメソッドが定義されていてそれを呼び出す際には、this.(メソッド名)
で呼び出すことができます。
引数も渡すことができます(やりかたは後述)。
renderSquareメソッド
を見ていくと、<Square />
とあるのでSquareコンポーネント
を見ていきます。
これはマス1つをレンダリングするものですね。
class Square extends React.Component {
render() {
return (
<button className="square">
{/* TODO */}
</button>
);
}
}
このようになっています。
ここは先程までの説明と同様ですね。
これで全体像が見えました。
Game
,Board
,Square
の3つのコンポーネントで構成されていて、しっかりとわけられていて、1つ1つがすっきりしています。
これがReact
のメリットの1つです。
こうすることで機能が増えてきても役割が分かれているのでカオスになりにくいです。
チュートリアル
propsを通してデータを渡す
まず手始めにマスの中に何かしらを表示していきます。
現状このように枠しか無いのを
このように数字を順番に表示させていきます。
手順の流れとしてはBoard
がSquare
に対してn番と表示されたマスを表示するという命令を出すようにします。
なのでBoard
からSquare
に引数を渡すようにします。
class Board extends React.Component {
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>
);
}
}
このように記述することで引数を渡すことができます。
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value}
</button>
);
}
}
受け取る側はこのようにすることで値を受け取ることができます。
これでマスの中に数字が表示されるはずです。
stateで状態をもたせる
次に、このままでは無いも変化がないのでなにもないマスをクリックしたときにX
が表示されるようにします。
その場合2つのポイントがあります。
- 現在のマスの状態をどこかに保持する
- クリックしたときのどうするかを実装する
です。
まず状態を保持できるようにしていきます。
Component
ではstate
を使って状態を保持できると先程書きました。それを今回実装していきます。
1つ1つのマスで自分自身のマスに何が表示されているかを保持するようにしていきます。なのでSquareコンポーネント
を書き換えていきます。
class Square extends React.Component {
//constructorを追加
constructor() {
super();
this.state = {
value: null,
};
}
render() {
return (
<button className="square">
{this.state.value} //props→stateに変更
</button>
);
}
}
state
を実装する場合constructor
を定義してこのように書いていきます。
constructor
では明示的にsuperメソッド
を呼び出す必要があります。
先程this.props.value
としていた部分をthis.state.value
とすることでstate
の値を表示するようにできます。
次にクリックしたときの挙動を実装していきます。
まずクリックしたときの挙動は
<button className="square" onClick={() => alert('click')}>
{this.props.value}
</button>
というように記述することで定義できます。
これはJavaScript(ES2015)
のアローファンクション構文を使用しています。
今回はマスをクリックするとアラートが表示されます。
今回はマスをクリックした際にstate
のvalue
をnull
からX
に変更したいです。
state
を変更する場合は
this.setState({value: 'X'})
というようにsetState
を呼べば値が変更されます。
なので最終的に
<button className="square" onClick={() => this.setState({value: 'X'})}>
{this.props.value}
</button>
というようにします。
実行してみると、このようになっているはずです。
状態を上に渡す
さて、state
をもたせることができましたが、この先に○
と×
を交互にでるようにしたり、勝敗が決まったかどうかを判定したりする機能を作る際に、現状のように各マスがstate
を保持している状態では管理がし辛いです。毎回各マスに問い合わせるというやりかたもできなくはないですが、理解しづらい破綻しやすいコードになってしまうのでReact
ではそうさせないようにしています。
結論としては、上の階層であるBoardコンポーネント
に状態をもたせるようにしたほうがいいです。
React
では親子のコンポーネントでデータをやり取りする場合には親に状態をもたせるようにするのがセオリーです。
ということでBoardコンポーネント
にstate
をセットしていきます。この際9マスの情報を集めるので9つの要素をもった配列で状態を表すようにします。
加えて、マスがクリックされたときにstate
も変化するようにしないといけません。
この場合、onclick
で実際に呼ぶ場所は子(Square
)、値を保持しているのが親(Board
)ということになるのでどのようにすればいいかというと、呼ばれたら状態を変更するという処理を親(Board
)で定義して、それを子(Square
)に渡すということをします。
なのでBoard
はこのようになります。
class Board extends React.Component {
//constructorを追加
constructor() {
super();
this.state = {
squares: Array(9).fill(null),
};
}
//handleClickを追加
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)} />; //squareの対応する要素を渡す,onClickを追加
}
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
は以下のようになります。
class Square extends React.Component {
//constructorを追加
constructor() {
super();
this.state = {
value: null,
};
}
render() {
return (
<button className="square" onClick={() => this.props.onClick()}> //state→propsに変更
{this.props.value} //state→propsに変更
</button>
);
}
}
これで変更が完了です。機能的には特に変わってはいないです。
なぜ普遍性が重要か
さてここで先程の変更で
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = 'X';
this.setState({squares: squares});
}
このような部分がありました。
クリックしたときにsquares
の対応する要素を変更するメソッドですが、squares
に変更を加えるのではなくsquares配列
をコピーしています。
直接変更してしまえばすぐ終わりな作業をわざわざこのようにしているのは意味があります。
そもそもプログラミングにおいて変数を極力使わずに定数のみを使ったほうがいいというセオリーがあります。というのは変更を許可すると思わぬところで値が書き換わっている可能性があるのでバグを生みやすいのとコードが複雑になって理解しにくなるからです。
ここで、React
では、state
の変更を検知してstate
を使用している各要素に変更を伝えて、値を受け取り、要素単位で考えたときに受け取った値が変更があれば再レンダリングをするという流れです。このために最小限の部分しか変更しないので描画が軽いという特徴があります。
もし変更が許可されたオブジェクトの変更を検知する場合には、全体のオブジェクトツリーをスキャンしてそれぞれの変数と値を比較する必要があります。これは複雑かつ時間がかかります。
一方変更が禁止されているオブジェクトでは参照されているオブジェクトが以前のものと同じかどうかを判定するだけでいいので変更の検知がしやすいです。
したがってReact
の特徴を最大限いかすために普遍性が重要になるわけです。
関数コンポーネント
Squareコンポーネント
はstate
を持たなくなったのでconstructor
は不要になったので削除します。
その場合Squareコンポーネント
はrenderメソッド
のみからなるコンポーネントとなります。React
ではこのようにrenderメソッド
のみからなるコンポーネントはfunctional components
として書くことができます。
function Square(props) {
return (
<button className="square" onClick={() => props.onClick()}>
{props.value}
</button>
);
}
Squareコンポーネント
はこのように書くことができます。
この場合this.props
と今までしてたのが、props
となるのに注意です。
○と×が交代する機能を作成
現状だとXしか表示されないので○
とX
が交代になるようにしていきます。
state
に次のプレイヤーがどちらになるかを保持するものを追加します。
class Board extends React.Component {
constructor() {
super();
this.state = {
squares: Array(9).fill(null),
xIsNext: true, //追加
};
}
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O'; //プレイヤーで分岐
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext, //○と×を交代させる
});
}
renderSquare(i) {
return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />;
}
render() {
const 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>
);
}
}
このようになったかと思います。
勝敗を判定する
次は勝負がついてきたときの表示を実装していきます。
勝敗がついたかどうかを判定する関数自体はもともと用意してくれていて、calculateWinner(squares)
が下の部分に実装されているかと思います。
react
でサーバーを建てて実行している場合はwarning
で今までcalculateWinner
を使ってないよっていうのがでててかと思いますがここから使用します。
Boardコンポーネント
を変更していきます。
勝敗がついているかどうかを判定して、ついていたら勝者を表示するようにします。
加えて、勝敗がついたときにそれ以降マスをおしても何も起こらないようにするのと、これまですでに埋まっているマスをクリックしても上書きできてしまっていたのですでに埋まっているマスをクリックしても何も起こらないようにします。
class Board extends React.Component {
constructor() {
super();
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
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.state.squares[i]} onClick={() => this.handleClick(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
という配列を作成して、その1つの要素に今までのsquares
を格納するようにします。そうすることで各動作ごとの状態を保持することができます。
そして、各動作のリストをどこで表示させるかを考えていきます。Boardコンポーネント
では3×3
のボードを作るのに専念させたほうがいいのでよりトップレベルのGameコンポーネント
にその役割を任せます。
そうした場合に、序盤でSquareコンポーネント
からBoardコンポーネント
に状態を引き上げたように、今回もBoardコンポーネント
からGameコンポーネント
に状態を引き上げていかないといけません。
Board
はこのようにconstructor
を削除して、state
を参照していた部分をprops
に変更します。
status
を表示していた部分はGame
がその役割を担うので削除します。
class Board extends React.Component {
renderSquare(i) {
return <Square value={this.props.squares[i]} onClick={() => this.props.handleClick(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>
);
}
}
Gameコンポーネント
は以下のようになります。
class Game extends React.Component {
constructor() {
super();
this.state = {
history: [{
squares: Array(9).fill(null)
}], //squaresを要素に持つ配列にする
xIsNext: true
};
}
handleClick(i) {
var history = this.state.history;
var 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,
});
}
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>
);
}
}
いままでrenderメソッド
しかなかったのをすべてここに集約するようになっています。
役割を移しただけなのでまだ機能としては変わっていません。
ここから履歴を表示するようにします。
class Game extends React.Component {
constructor() {
super();
this.state = {
history: [{
squares: Array(9).fill(null)
}], //squaresを要素に持つ配列にする
xIsNext: true
};
}
handleClick(i) {
var history = this.state.history;
var 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,
});
}
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');
}
//追加
const moves = history.map((step, move) => {
const desc = move ?
'Move #' + move :
'Game start';
return (
<li>
<a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
</li>
);
});
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> //movesに変更
</div>
</div>
);
}
}
このように変更します。
React
で複数のアイテムをレンダリングする際の一般的な方法は、配列のデータをmap
することです。
map関数
でDOM
を返すようにしていきます。
これを実行するとこのようにwarning
が出てきてしまいます。
これにはある理由があります。
Key
React
ではレンダリングを最小限にするために変更されたものしか再レンダリングをしないような仕組みになっています。
先程のwarning
の原因は、リストを再レンダリングを最小限にするための仕組みがどうなっているかを考えてみると見えてきます。
このようなリストがあったとします。
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
それがこのように変更したとします。
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
人間の目で見た際には順番が変わって、1つ要素が増えて、中身の値も一部が変わっているというようにわかりますが、React
はこれだけでは今のようなことを認識してくれるわけではありません。
そこでReact
ではリストの各要素にkeyプロパティ
を指定することで順番が変わったとしても新しく追加されたとしても、中身の値を変更する必要があるかどうか認識することができます。
認識するためにはkey
はユニークである必要があるので今回は、alexa
,ben
,claudia
をkey
とするのがよいです。上から順番に1からふっていくというようなやり方は順番が変わったとしても上から順番に1から振られてしまうので適切ではありません。DB
のオブジェクトと一致する場合はDB
のID
をkey
として使うのが適切です。
このようにReact
ではアイテムのリストをレンダリングする際に常にリスト内の各アイテムの情報を記録しているので、要素数が多かったとしても再レンダリングする必要があるアイテムだけ再レンダリングされます。
マルバツゲームの実装に戻ると、このwarning
を解消するためには今までの話を踏まえるとkey
を要素に追加すればいいわけです。
const moves = history.map((step, move) => {
const desc = move ?
'Move #' + move :
'Game start';
return (
<li key={move}> //keyを追加
<a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
</li>
);
});
これでwarning
が解消されたかと思います。
jumpToを実装
現状だとボタンを押してもjumpToメソッド
を定義していないので実装していきます。
class Game extends React.Component {
constructor() {
super();
this.state = {
history: [{
squares: Array(9).fill(null)
}], //squaresを要素に持つ配列にする
xIsNext: true,
stepNumber: 0 //追加
};
}
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
}]),
xIsNext: !this.state.xIsNext,
stepNumber: history.length //追加
});
}
//追加
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) ? false : true,
});
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];//変更
const winner = calculateWinner(current.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
//追加
const moves = history.map((step, move) => {
const desc = move ?
'Move #' + move :
'Game start';
return (
<li>
<button onClick={() => this.jumpTo(move)}>{desc}</button> //変更
</li>
);
});
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>
);
}
}
これですべて完成です!
完成品はこの様になっています。
コード類はhttps://github.com/fkt1993/react-sample-appここで管理もしているので良かったら参考にしてください。
追加課題
チュートリアルの内容は一通り完成しましたが、一番最後に以下のように追加課題が課せられています。
1. それぞれの遷移に(col,row)の表示をする
2. 遷移リストの現在選択されているものを太字に
3. Boardをloop2回つかって書いてみる
4. 遷移リストを昇順/降順にする
5. 誰かが勝った時に、3つ揃ったセルをハイライトする
6. 引き分けの時にはdrawを表示
公式のチュートリアルでは追加課題は課題が課されるだけで実際の答えのようなものは存在しません。
こちらの記事で、追加課題をやってみたものをまとめてみたので、もし興味があれば見てみてください。
おわりに
今回React.js
のチュートリアルを行いました。内容を見ていくと単なるチュートリアルだけではなく概念など細かい部分にも言及されていたので、理解のために非常に助けになりました。
(2024/09/15 08:54:26時点 Amazon調べ-詳細)