Reactの理解がまだまだ浅いと感じ本家のチュートリアルをやってみる事にする。
reactはjsのライブラリになるのでjsが不安な時は一度jsを復習するべきである
英語だがこちらがES6を含めて復習できるのでオススメ
→https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript
開発環境の準備
コマンドラインで
npx create-react-app sanmoku
を実行する。
それから元々あるsrcフォルダーの中身をすべて削除する。
それからindex.css
とindex.js
ファイルを作成する。
そのindex.jsにreact・reactDOM・cssを読み込む
//index.js
import Reeat from 'react';
import ReactDOM from 'react-dom';
import './index'
この後公式ではnpm start
で三目盤が表示されると書いてあるが、今回はまだ記述をしてないのでサーバーを立ち上げても何も表示されない。
あくまでも今回はreactを色々触ってみるのが目的なのでcssとjsは公式の物をそのまま使う。
後々はすべて自分で書けるようになりたいと思う。
##データをpropsで渡す
BoardというrenderSquera
メソッド内でpropsとしてvalueという名の値をBoardからSquareに渡す。
renderSquare(i) {
return <Square value={i} />;
}
それを受け取るためにSquareのrenderメソッド内で渡された値を表示するため
render() {
return (
<button className="square">
{this.props.value}
</button>
);
}
に書き換える。
これでBoardコンポーネントから子コンポーネントであるSquareコンポーネントにpropsを渡すことに成功した。
##インタラクティブなコンポーネントを作る
Squareコンポーネントがクリックされた時に✖になるようする。
そのためにはSquareコンポーネントにクリックされた場所を✖マークで埋めるにはクリックされた場所を記憶させる必要がある。
そこで使うのがstate
である。
stateを設定することで状態を管理することが出来る。
コンストラクタ(関数)内でthis.state
を設定すると状態を管理することが出来る。
javascriptのクラスはサブクラスのコンストラクタを定義する時に毎回super
を呼ぶ必要がある。
→https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
次にSquareのrenderメソッドを書き換えて、クリックされた時に今のstate=nullから変更する。
render() {
return (
<button
className="square"
onClick={() =>
this.setState({ value: 'X' })}
>
{this.state.value}
</button>
);
}
onClickハンドラ内でthis.setStateを呼び出すことでbuttonがクリックされたら常に再レンダーをするように伝えられる。
そのthis.setStateの中のvalue
が✖にしたので、どこのマスをクリックしても✖になる。
##Stateのリフトアップ
今はそれぞれのマスがそれぞれの状態を持っている。
しかしそれではゲーム判定が出来ないので9個まとめて管理する必要がある。
今回は親であるBoardコンポーネントで管理する。
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
~~~
}
Boardにコンストラクタを追加して初期のstateとして9個のマス目に対応する9個のnull値をセットする。
またrenderSquare
メソッドはこのようになっている。
しかし今はSquare自体に✖
を設定していたためBoardからのvalueを受け取る事はなかった。
これをBoardから受け取るようにセットする。
renderSquare(i) {
return <Square value={this.state.squares[i]}
/>;
}
とすることでBoardで設定したvalueをSqareに渡すことが出来る。
###次にマスがクリックされた時の挙動を変更する。
今、どのマスに何が入ってるかを管理しているのはBoardである。
そのstateをSquareが更新できるようにする必要がある。
しかしBoardで設定されているstateはローカルの物なので異なるコンポーネントであるSquareからでは書き換える事が出来ない。
代わりにBoardからSquareに関数を渡すことでマスがクリックされた時にSquareにBoardの関数を呼ぶことで擬似的にSquareからBoardのstateの変更を可能にする。
renderSquare(i) {
return <Square value={this.state.squares[i]}
onClick={() =>
this.handleClick(i)}
/>;
}
これでBoardからSquareにはpropsとしてvalue
とonClick
2つの値を渡している。
onClick
はマスがクリックされた時にSquareが呼び出すものである。
Squareを以下のように書き換える。
class Square extends React.Component {
render() {
return (
<button
className="square"
onClick={() =>
this.props.onClick()}
>
{this.props.value}
</button>
);
}
}
こうすることでSquareがクリックされた時にBoardから渡されたonClickをコールして変更をすることが出来る。
流れとしては
1.button
がクリックされると設定されているonClick
が起動する。
2.button
のonClick
に設定されているBoardでセットしたonClickを呼び出す
3.renderSquareのonClickでセットしたhandleClick
を呼び出す。
4.handleClickは定義してないのでエラー表示が起きる
handleClickをBoardで定義する。
handleClick(i) {
const squares =
this.state.squares.slice();
squares[i] = 'x';
this.setState({ squares: squares });
}
これによってすべてのマスをBoardで管理することで勝敗判定を出来るようになる。
Squareコンポーネントは自分でsateを管理せずBoardが管理するのでreactの単語でSquareコンポーネントは制御されたコンポーネントと呼ばれる。
そしてhandleClick内ではsquaresを直接変更する代わりにslice()
を読んで配列のコピーを作成している。
##なぜ直接変更ではなく変更したコピーを作成するのか
一般的に変化するデータへのアプローチは2通りある。
・変更するデータを直接書き換える
・変更を加えた新しいデータを古いデータと置き換える
最終的な結果かは同じだが直接変更しないメリットがいくつかある。
・複雑な機能が簡単に実装できる
・変更の検出
・再レンダーのタイミング決定
##関数コンポーネント
stateを持たないシンプルなコンポーネントである。
Squareを関数コンポーネントで書き換える。
function Square(props) {
return (
<button className="square" onClick=
{props.onClick}>
{props.value}
</button>
)
}
##ターンの処理
今まで〇が出てきてなかったので〇が出るように修正する。
デフォルトで先手を✖にする。
Boardのコンストラクタでstateの初期値を変更すればデフォルト値は変更できる。
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
プレイヤーが着手するたびにxIsNext
の真偽値を反転させてゲーム状態を保存する。
BoardのhandleClick関数を書き換えてxIsNext
の値を反転させる。
三項演算子を使って書く。
三項演算子について→https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Conditional_Operator
handleClick(i) {
const squares =
this.state.squares.slice();
squares[i] = this.state.xIsNext? 'x':'o';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
1つずつ理解していく
squares[i] = this.state.xIsNext? 'x':'o';
1ターン目を例にする。
さっきコンストラクタでxIsNext:true
と書いたので、この三項演算子ではx
を取りクリックした場所に✖を置くようにする。(これだけでは✖は画面に表示されない)
それから下のコードで次のターンに移行する
this.setState({
squares: squares,//1
xIsNext: !this.state.xIsNext,//2
});
1では新しい✖を加えたデータを古いデータと置き換えるコードである。
2は次に〇のターンにするため!
を使う事でthis.state.xIsNext
の反対(今回はtrue→false)をxIsNext
に入れて〇のターンにする。
これによって交互に置くことが可能になった。
##勝者判定
決着判定のために以下の関数をファイルの最後に追加する。
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;
}
ここについては後ほど
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c])
Boardのrender内でcalculateWinner(squares)
を呼び出して勝敗がついてるかを確認する。
同時にstatus
も書き換える。
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status='winner:'+winner
} else {
status ='Next player: '+ (this.state.xIsNext?'×':'〇');
}
~~~
}
後は
・ゲームの決着がついてる時
・すでに置かれてるマスをクリックした時
には何も変化せず済むように記述する。
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,
});
||
は論理演算子orである。
##最低限完成
これでオセロゲームとしては最低限遊べるようになった。
##タイムトラベルの実装
もしsquaresの配列を直接書き換え(ミューテート)していたらタイムトラベルの実装はとても難しかった。
しかし今回は配列のコピーを作り、配列をイミュータブルなものとして扱った。だからsquareの過去のデータを遡ることが出来る。
過去のsqusresの配列をhistory
という名前の別の配列に保存する。
(二次元配列になる)
###リフトアップ
今まではマス自体を管理していたのでBoardで管理していたが、これからはボード全体を管理する必要があるので更に一段階リフトアップする必要がある。
そのためGame
コンポーネントがhistory
にアクセスできる必要があるのでトップレベルのGameコンポーネントでhistoryというstateを置く。
history
stateをGameコンポーネント内に置くことでBoardコンポーネントからsquare
stateを取り除ける。
これでBoardコンポーネントのデータを制御できるようになり過去の盤面をレンダー出来るようになる。
Gmaeコンポーネントにhistory
sstateとターンを決めるxIsNext
stateを置く
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares:Array(9).fill(null)
}],
xIsNext: true,
}
}
~~~
}
次にBoardコンポーネントが`squaresと
onClick``プロパティをGameコンポーネントから受け取るようにする。
Board内には9個のマスにそれぞれ対応するクリックハンドラがあるのでSquareの位置を``onClick``ハンドラに渡してどのマスがクリックされたかを伝えるようにする。
これらをするには以下の作業が必要である。
・Boardのconstructor
を削除する
・Board内のrenderSquares
の中身の書き換え
class Board extends React.Component {
renderSquare(i) {
return <Square value={this.props.squares[i]}
onClick={() =>
this.props.handleClick(i)}
/>;
}
~~~
}
これでGameコンポーネントのrender関数を更新することで画面を表示できるようになった。
だからBoard内のrender関数はreturn
を除いて削除できる。
最後にhandleClick
メソッドをBoardコンポーネントからGmaeコンポーネントに一部修正して移動する。
このhandleClickにもhistory
を追加する。
##過去の着手の表示
これで三目並べの履歴を記録してるので画面に表示させることが出来る。
mapメソッドを使って配列をマップして画面上にボタンを表現するreactを作る。
const winner = calculateWinner(current.squares)
const move = history.map((step, move) => {
const desc = move ? 'Go to move#' + move : 'Go to game start'
return (
<li>
<button onClick={() => this.junpTo(move)}>{desc}</button>
</li>
)
})
let status;
if (winner) {
status='winner:'+winner
} else {
status ='Next player: '+ (this.state.xIsNext?'×':'〇');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
oncClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{move}</ol>
</div>
</div>
);
}
ゲームのターン毎に対応したbutton
を持つli
を作る。
ボタンにはonClick
ハンドラを持ちthis.jumpTo()
というメソッドを呼び出す。
同時にリストにkeyを設定するように言われた。
最悪なくても動くw
後jumpTo
がまだ実装されていないのでそちらも実装していく。
その前にGameコンポーネントのstateにstepNumber
を加える。
これは今何手目かを判断するのに使う。
##タイムトラベルの実装
まずkeyを設定する。
const desc = move ? 'Go to move#' + move : 'Go to game start'
return (
<li key={move}>
<button onClick={() => this.junpTo(move)}>{desc}</button>
</li>
)
後stepNumbeer
の初期値を設定する
constructor(props) {
super(props);
this.state = {
history: [{
squares:Array(9).fill(null)
}],
xIsNext: true,
stepNumber: 0,
}
}
次にjumpTo
メソッドを定義する。
stepNumberを更新されるようにして、それが偶数だったらxIsNextがtrueになるよう設定する。
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext:(step %2)===0,
})
}
次にマス目をクリックした時に実行されるhandleClickメソッドをいくつか変更したら完了である。
・stepNunberの更新
・新しい手を打った時に更新できるようにする。
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,
});
}