前回まではProgateの無料レッスンを一通りこなしました。
今回からはアプリを作成するアウトプットを行っていこうと思います。
または、自分が新たに習得した知識の共有等も行えたら良いなと思います。
React公式チュートリアル
Reactの公式チュートリアルに三目並べゲームがあり、なぜかこれが気になってしまうので、作っていきたいと思います。
最終成果物は → 三目並べゲーム
これをチュートリアルに沿って作るのが目標です。
前提知識
アロー関数、クラス、let
およびconst
が理解できていることが前提らしい。
完璧には理解できていないが、わからなければググるので問題なし。
チュートリアルの準備
開発環境は以下の2つ
- ブラウザで書く
- ローカルに開発環境を構築して書く
以前Progateの無料レッスンをやっているので、ローカルに構築済みです。
Progate無料版をやってみる【React】
以下のソースコードを元に作成する模様。
https://codepen.io/gaearon/pen/oWWQNa?editors=0010
3つのコンポーネントで構成される
- Square: マス目を表し、buttonタグをレンダー
- Board: 9個Squareをreturnしている。マス目全体
- Game: あれ、これ何を表しているんだ?
チュートリアルには「後ほど埋めることになるプレースホルダーを描画しています」とのこと。後ほど書くのね。
とりあえず元となるソースをコピペまたは模写していきます。
index.jsを変更しちゃいます。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import * as serviceWorker from './serviceWorker';
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 palayer: 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="gmae-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<div>{/* TODO */}</div>
</div>
</div>
);
}
}
ReactDOM.render(
<React.StrictMode>
<Game />
</React.StrictMode>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
一番下のserviceWorker.unregister();を消すとエラーになっていた。
以前のインストール時のHello World的なチュートリアルで入ってしまった模様。
キャッシュらしいです。よくわからない・・・。
参考にさせていただきました。
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;
}
Propsでデータを渡す
Props(properties)を用いて親から子に値を受け渡す。
Board(親)からSquare(子)に値を受け渡します。
渡す値は、Board.renderSquareの引数i
です。
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />;
}
class Square extends React.Component {
render() {
return <button className="square">{this.props.value}</button>;
}
}
プロジェクトトップに移動し(cd 移動先
)
npm start
します。
そしてhttp://localhost:3000
にアクセスします。
おお!でた。
Boardから数字が渡ってきてマスの中に表示されているんですね。
マス目をクリックされたときのイベントを設定する
jQueryだと
$('button.square').click(() => {
// 何か処理
});
素のJavaScriptだと
document.querySelectorAll('button.square').forEach((node) => {
node.addEventListener('click', () => {
// 何か処理
});
});
となり、HTMLとは別個に記述します。
JSXの場合HTMLと一緒に書くようです。
class Square extends React.Component {
render() {
return (
<button
className="square"
onClick={() => {
// 何か処理
}}
>
{this.props.value}
</button>
);
}
}
た、多分JSXのほうがイイヨネ・・・。
今はこの独特の書き方が慣れてないんで微妙ですが、たくさん書けば良さを実感できるんでしょうか。
マス目の中身をXに変更する処理は、jQueryや素のJavaScriptのようにDOMAPIを呼ぶ書き方ではなく、stateを用いる方法で実現します。(コンパイル後には結局DOMAPIを呼ぶ書き方に変わりますが)
Square
・constructor
を追加し、引数にprops
を設ける
・super(props)
で継承元のコンストラクターを呼ぶ。お決まり
・state
にvalue
プロパティを持つオブジェクトを設定。value
プロパティの値はprops.value
を設定
・onClick
内ではsetState
する
・レンダー時のマス目の値はprops.value
からstate.value
に変更する
class Square extends React.Component {
constructor(props) {
super(props);
this.state = { value: this.props.value };
}
render() {
return (
<button
className="square"
onClick={() => {
this.setState({ value: 'X' });
}}
>
{this.state.value}
</button>
);
}
}
結果
ブラウザで確認時、Reactのコンポーネントツリーを調べる拡張機能があるらしい。
React Devtools 拡張機能
マス目の制御を行う
現在のマス目の内容の状態を取得するには、Board側から各Squareに取りに行くイメージでやればいいと思いがちですが、可読性が落ちたり、バグが起きやすいとのことです。
Board側で各Square側の状態を保持しておいて、propsを用いてやり取りするのがベストらしいです。
具体的にはまず、Board側にSquareに渡す値の情報を保持します。
→ マス目情報を長さ9つの配列として保持します。
Boad側
class Board extends React.Component {
constructor(props) {
super(props);
this.state = { squares: Array(9).fill(null) };
}
renderSquareメソッドで配列のi番目を渡すようにします。
renderSquare(i) {
return <Square value={this.state.squares[i]} />;
}
そしてSquareからBoardを更新してもらうために、Squareに対して更新用の関数を渡してあげます。
上のvalue={this.state.squares[i]}
に続いてonClick
を定義。
renderSquare(i) {
return (
<Square
value={this.state.squares[i]} // クロージャでiが保持されている
onClick={() => {
const squares = Object.create(this.state.squares); // 別の配列としてクローン
squares[i] = 'X';
this.setState({ squares: squares });
}}
/>
);
}
更新処理が書かれたこの関数ごとSquareに渡るイメージです。
Square側のpropsに入っているイメージ。
iについて
renderSquare(i)
のところのi
はクロージャによって、最初にレンダーされた時の各Squareごとの値を保持しています。
クロージャ
onClick内の更新処理について
Reactのstateは直接更新は推奨されていないので、一度Object.create
でコピーを作ってから、そのコピーを書き換えて、setSate
で更新しています。
なぜ推奨されないか
チュートリアルではコピーのところはthis.state.squares.slice()
でしたが、これは配列の中身がプリミティブ値(数値、文字列、真偽値)の1次元配列の時のみ有効です。配列の中にオブジェクトや、さらに配列が入っていたら、完全なコピーができません。(ディープコピー)
コピー元の配列(state)と同じメモリの場所を参照してしまい、この後これに変更を行ってしまうと、「元のstateを変更しない」に反します。
このチュートリアルでは1次元の数値の配列なのでOKですが、今後それ以外のケースが出てきたときに誤ってsliceを使わないようにObject.create
で慣れておこうと思います。
Array.prototype.slice()
Square側
・onClickの中で自身のstateを更新していたのをBoardから渡ってきたpropsのonClickを呼ぶように変更します。
・さらに、マス目に表示していたthis.state.value
はthis.props.value
に変更します。
これを行うことにより、ボタン押下時のBoard側で変更されたstateの値がこのpropsに反映されます。
・stateを持つ必要がなくなったので、constructor
を削除します。
class Square extends React.Component {
// constructorは不要になったので削除
render() {
return (
<button
className="square"
onClick={() => {
this.props.onClick(); // BoardのonClickを呼び出す。
}}
>
{this.props.value} // state → propsに変更
</button>
);
}
}
ここまでのBoardとSquareの全文
class Square extends React.Component {
render() {
return (
<button
className="square"
onClick={() => {
this.props.onClick();
}}
>
{this.props.value}
</button>
);
}
}
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={() => {
const squares = Object.create(this.state.squares);
squares[i] = 'X';
this.setState({ squares: squares });
}}
/>
);
}
render() {
const status = 'Next palayer: 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はstateを持たなくてよくなったので、React.Componentを継承する必要もなくなります。
もっと簡潔に書けるようになるみたいです。
function Square(props) { // 継承がなくなった
return (
// onClickのところがシンプルに
// あとpropsにthisがいらない。引数で来ているので
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
手番の処理(XやOを実装)
先手はX
にします。
Boardに「次の手番はだれか」を判定するためのフラグを設けます。
→「次の手番はX
だ」というフラグを設けて、その値のtrue
false
で判定します。
その判定により、X
とO
を代入します。
Board
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true, // 先手はXなので初期値true
};
}
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => {
const squares = Object.create(this.state.squares);
// xIsNextがtrueならX、それ以外なら○
squares[i] = this.state.xIsNext ? 'X' : 'O';
// xIsNextは現在の反対を代入することで手番が変わる
this.setState({ squares: squares, xIsNext: !this.state.xIsNext });
}}
/>
);
}
ついでに画面に表示していた文字のところのNext palayer:
も動的に書き換わるようにします。
render() {
const status = 'Next palayer: ' + (this.state.xIsNext ? 'X' : 'O');
あれ?でもこれここのstatus
を書き換えて意味あるの?って思っちゃいました。
なぜなら動的に変更するものはstate
に入れておかないとダメとおもっていたので・・・。
このrender
がマス目を毎回クリックするたびに毎回動くなら大丈夫ですけど・・・。
→ 確認したら毎回動いてました・・・。
でも毎回動くのって無駄なような気が・・・。
毎回このhtmlを作成しているってことですよね。
このチュートリアルは要素数が少ないからいいですけど、普通のwebアプリを作ろうとしたらめちゃめちゃ要素数多いページとかあるのにレスポンス大丈夫なんでしょうか・・・。
後日調べたいと思います。
ゲーム勝者の判定
動かしてみると分かりますが、今のままだとゲームが終わった後に何度でも入力が出来てしまいます。
なので、勝敗が着いたら何もさせないような制御を実装します。
また、どっちが勝ったかを分かり易くする為に、「勝者: ○○」の文字を表示させようと思います。
チュートリアルでは勝敗を判定してくれる関数を用意してくれています。これをコピーして貼り付けます。
チュートリアルではBoardの外に書いてますが、私は中に入れました。(renderの下)
// 勝敗判定関数
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;
}
勝者を表示させるNext palayer:
の部分を変更します。
render() {
const winner = this.calculateWinner(this.state.squares);
let status;
if (winner) {
status = '勝者:' + winner;
} else {
status = '次の手番: ' + (this.state.xIsNext ? 'X' : 'O');
}
最後に
- マス目が押された時に勝敗が決まっていたら
- 既にマス目が埋まっていたら
何も処理しないようにします。
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => {
const squares = Object.create(this.state.squares);
if (this.calculateWinner(squares) || squares[i]) {
// 何もせずにreturn
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({ squares: squares, xIsNext: !this.state.xIsNext });
}}
/>
);
}
ifのところは
左の式は、this.calculateWinner(squares)の戻り値がnull
以外なら勝敗が決したということです。
右の式は、suare[i]
がnull
以外なら既にマス目に値が入っているということです。
以下、完成した全文
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import * as serviceWorker from './serviceWorker';
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => {
const squares = Object.create(this.state.squares);
if (this.calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({ squares: squares, xIsNext: !this.state.xIsNext });
}}
/>
);
}
render() {
const winner = this.calculateWinner(this.state.squares);
let status;
if (winner) {
status = '勝者:' + winner;
} else {
status = '次の手番: ' + (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) {
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;
}
}
class Game extends React.Component {
render() {
return (
<div className="game">
<div className="gmae-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<div>{/* TODO */}</div>
</div>
</div>
);
}
}
ReactDOM.render(
<React.StrictMode>
<Game />
</React.StrictMode>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
おわりに
ここまでで一旦、三目並べゲームが完成です。
Qiitaに書きながらなのでなかなか苦労しました・・・汗
チュートリアルではこの後「タイムトラベル機能」の追加の手順があるので、次回はそちらをやっていこうと思います。
履歴から手順を戻すやつですね。
GitHub・・・公式にソースあるしいらないかw
→ 次回