Reactチュートリアル始めました。
自分の理解の整理がてら、解釈をまとめていきます。
データを Props 経由で渡す
★propsとは:クラスからインスタンスを作るときに、個々のインスタンスに渡す中身のこと。
「props名=値」という形で、コンポーネントを呼び出すときに渡す。
★コンポーネント:「構成要素」。サイトを構成するパーツ。関数を使って作る方法と、クラスを使って作る方法がある。
//Squareクラス: 1マスの設計図
class Square extends React.Component {
render() {
return (
<button className="square"> //1マスがそれぞれ正方形のボタンになってる。
{this.props.value} //ボタンの中の文字を表現。このオブジェクトに渡されたvalueが表示される
//this: この/生成されたSquareクラスのインスタンスの
//props: propsの
//value: value(という名前のprops)の値
//を表示します
</button>
);
}
}
//Boardクラス: 盤面(3x3のマスの集合体)の設計図
class Board extends React.Component {
//renderSquare()という関数を定義
renderSquare(i) {
//引数を i で受け取って、valueというpropsの値に渡す
return <Square value={i} />;
}
render() {
const status = 'Next player: X';
return (
<div>
<div className="status">{status}</div>
//↑で作ったrenderSquare()を呼び出して1マスずつ作成。
<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 コンポーネントに自分がクリックされたことを「覚えさせ」て、“X” マークでマスを埋めるようにさせます。コンポーネントが何かを「覚える」ためには、state というものを使います。
★state: 状態。ここでは1個1個のマスにクリックされたかどうかの状態を持たせるようにする。
★constructor: クラスからインスタンスを生成するとき(new
した時)に実行される関数。constructor() { }
の中括弧内に実行したい処理を書く。
class Square extends React.Component {
constructor(props) {
super(props);
this.state = { //this(生成されたインスタンス)のstate(状態)に以下を初期値として代入
value: null, //valueという名前の(マスにクリックされたかどうかの)状態はnullとしておく
};
}
render() {
return (
<button className="square" onClick={() => alert('click')}> //←※
{this.props.value}
</button>
);
}
}
これでvalueの初期値を設定したので、今度はマスをクリックした時に、このvalueが「X」に変わるようにする。
(現状ではクリックするとアラートが出るようになっている(※印部分))
.
.
.
render() {
return (
<button className="square" onClick={() => this.setState({value: 'X'})}>
//マス(button)をクリック(onClick)して、このインスタンスのステータスをセット(setState)する、valueをXとして。
{this.state.value} //このstateのvalueを表示する
</button>
);
}
//以下略
stateのリフトアップ
マスを推してXが出るようになった!わーい、と思いきや、これではまだまだ、マスの1個1個がバラバラに機能しているだけで、ゲームの順番も、勝敗も何もない状態です。
そこで、1個1個のマスの親コンポーネントである盤面(Board)さんに子マスちゃん(Square)たちの状態を管理させることにします。
そのために登場するのが、一番最初に出てきたprops
。
親コンポーネントは props を使うことで子に情報を返すことができます。こうすることで、子コンポーネントが兄弟同士、あるいは親との間で常に同期されるようになります。
そんなわけで、Squareで設定していたstateをBoardの方に移していきます。
class Board extends React.Component {
constructor(props) {
super(props);
this.state = { //Boardのステートに値を代入
squares: Array(9).fill(null),
// squaresというステートを持たせて、初期値として、9つ(マスの数)の配列を代入。(初期値として、配列の中身は全部null)
};
}
renderSquare(i) {
return <Square value={this.state.squares[i]} />;
//1個のマスを生成して返す。 マスの値は、このBoardのステートでsquaresの配列i番目の値を入れるように
}
.
.
.
次に、マスをクリックして、表示(Boardで管理するようにしたstateの値)を変える設定を、
Squareの中から、Boardで定義しているrenderSquare
の中に移します。
なぜなら↓↓
state はそれを定義しているコンポーネント内でプライベートなものですので、Square から Board の state を直接書き換えることはできません。
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)}
//クリックしたら、このインスタンスに対してhandleClick関数を呼び出します(引数iを渡す)
//handleClickは未定義
/>);
}
.
.
.
Board(親)の方での準備ができたので、今度はSquare(子)の方で、
propsを介して情報の受け渡しをできるようにしていきます。
class Square extends React.Component {
//↓Squareではstateを管理しなくなったので、コンストラクタは消す
// constructor(props) {
// super(props);
// this.state = {
// value: null,
// };
// }
render() {
return (
<button
className="square"
onClick={() => this.props.onClick()}
//マスがクリックされると、Boardの方で設定したonClickプロパティが呼ばれる。
>
{this.props.value}
//このpropsのvalueが表示される
</button>
);
}
}
ここまでできたら、実際にBoardのstateを書き換える処理(さっき未処理としていたhandleClick)を書いてきます。
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という定数を定義。squaresにはこのBoardのstateである、squaresを代入。
//(slice関数で新しい配列を作ってる)
squares[i] = 'X';
//↑で定義したsquaresのi番目にXを代入。
this.setState({squares: squares});
//このBoardに対して、squaresというstateの値を、 ↑で定義した定数squares(i番目にXが入れられた)として、セットする
}
.
.
.
※配列に対して呼び出されたslice()
メソッドは、元の配列を(引数で指定された範囲で)切り出して新しい配列を作る。
startが指定されなかった場合は、最初から、
endが指定されなかった場合は、最後まで、を取り出すことになるので、
引数に何も入っていないと、元の配列と同じ内容のコピーを作ることになる(という認識)
関数コンポーネント
マスの状態(state)の管理はBoardで行うようになったので、
SquareはBoardとの情報の受け渡しだけすれば良いようになりました。
こうなるとクラスを定義するまでもないので、Squareは情報の受け渡しという処理をする関数としてのコンポーネントに書き直します。
function Square(props) {
return(
<button className="square" onClick={props.onClick}>
//クラスコンポーネントだったときはthisで受け取っていたけど、関数にしたので、
//引数として受け取ったpropsに対してonClickを呼ぶ
{props.value}
//同じく引数として受け取ったpropsのvalueを表示する
</button>
);
}
手番の処理
続いて、現状ではXしか打てないようになっているので、XとOを交互に打てるようにしていきます。
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
//stateに xIsNext というstateを追加。初期値はtrue
};
}
handleClick(i){
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
//stateのxIsNextが trueなら'X'、falseなら'O' をsquares[i]に代入する
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
//xIsNextの値を、今のxIsNextの状態から反転させる
});
}
.
.
.
Boardのrender内の次のプレーヤーの表記も同様に、書き換え。
ちなみにこの書き方
条件文 ? 条件がtrueの場合の処理 : falseの場合の処理
は三項演算子というもので、単純なif文を書くときに便利。
class Board extends React.Component {
.
.
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
//↑文字列と式を連結するときは括弧で囲む
.
.
ゲーム勝者の判定
まずは勝者を判定するヘルパー関数を作ります。
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++ ) { //linesの数だけ繰り返す
const [a, b, c] = lines[i]; //[a, b, c]にlinesを一個一個代入して
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
//squares[a]のマスが埋まってて、bともcとも値が同じだったら
return squares[a];
//squares[a]のマスの値(XorO)=勝者 を返す
}
}
return null;
//for文最後まで行っても何も返らなかったら(勝者が決まらなかったら)nullを返す
}
続いて、Boardのrender内で上で作った勝者判定メソッドを呼び出して、
誰が勝ったか目にみえるようにします。
class Board extends React.Component {
.
.
render() {
const winner = calculateWinner(this.state.squares);
//勝者判定メソッドを呼び出して結果をwinnerに代入
let status;
if (winner) { //勝者が決まってたら
status = 'Winner: ' + winner; //statusは勝者を表示
} else { //決まってなかったら
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
//次のプレーヤーを表示
}
.
.
これで完成したかのように見えますが、もうひと手間。
実はすでに埋まってるマスをクリックすると書き換えられてしまうので、いつまで経ってもゲームが終わりません。
そこで、handleClickメソッドを書き換えて、勝敗が決まっているもしくは、マスが埋まっているとクリックできないようにします。
class Board extends React.Component {
.
.
.
handleClick(i){
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
//勝者が決まっている か マスが埋まっていたら
return;
// return 処理をここで終了
}
squares[i] = this.state.xIsNext ? 'X' : 'O' ;
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
これでひとまず、ゲームの勝敗がつくところまで完成です!
タイムトラベル機能の追加
ちょっと頭がこんがらがったので、一個ずつ解いていこうと思います。
まず、タイムトラベル機能がどんなものかというと、
お手本の右側に表示されてるやつですね。
1手1手打つごとに、Go to 〇〇
というボタンが増えていって、
ボタンを押すと〇〇番目の盤の状態に戻ることができる、という機能です。
仕組みとしては、
- 1手ごとに盤の状態(どこにXやOが入っていて、どこが空欄なのか)を保存。
- その盤の状態を持ったボタンを生成して表示。
- ボタンを押すと対応する盤の状態を反映。
- ある手に戻った段階でマスをクリックすると、その手よりあとの盤の状態はクリアされる。
というものです。
というところで一つ一つ作っていきましょう。
着手の履歴の保存
1手ごとの盤の状態を保存します。
Boardにstateの管理を移した時のことを思い出しましょう。
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という定数を定義。squaresにはこのBoardのstateである、squaresを代入。
//(slice関数で新しい配列を作ってる) ←←← ココ!!!!!!!!!!!
squares[i] = 'X';
//↑で定義したsquaresのi番目にXを代入。
this.setState({squares: squares});
//このBoardに対して、squaresというstateの値を、 ↑で定義した定数squares(i番目にXが入れられた)として、セットする
}
マスをクリックした時の処理として、
現状のBoardのステータスをコピーし、コピーしたものに新たな手番を代入し、改めてsetStateしていました。
(=イミュータブルなものとしてsquaresを扱っていた)
なので、コピーする前のsquaresというステータスを別に取っておけば、盤の状態を一回ごとに保存できるようになりそうです。
というわけで、この手番ごとのsquaresたちを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',
]
},
// ...
]
リフトアップ再び
さて、このhistoryという配列は、トップレベルのGameコンポーネント内で履歴を表示できるようにしたいので
Gameコンポーネントに置くことにします。
history配列の中に保存していくsquaresたちはBoardコンポーネントのstateでしたが、
Gameコンポーネントで扱うようになるので、Boardコンポーネントからリフトアップしていく必要があります。
まずは、Gameコンポーネントのコンストラクタに初期stateをセットします。
class Game extends React.Component {
contructor(props) {
super(props);
this.state = {
history: [{
//↑Gameコンポーネントではhistory配列として扱っていくのでBoardコンポーネントの時と比べてここが増える
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}
//以下略
}
Gameコンポーネントにコンストラクタをうつしたので
Boardコンポーネントからはコンストラクタを削除します。
class Board extends React.Component {
//↓消す
// constructor(props) {
// super(props);
// this.state = {
// squares: Array(9).fill(null),
// xIsNext: true,
// };
// }
handleClick(i){
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
//stateのxIsNextが trueなら'X'、falseなら'O' をsquares[i]に代入する
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
//xIsNextの値を、今のxIsNextの状態から反転させる
});
//以下略
そして続けて、
- Board の renderSquare にある this.state.squares[i] を this.props.squares[i] に置き換える。
- Board の renderSquare にある this.handleClick(i) を this.props.onClick(i) に置き換える。
をしろと書いてあります。
、、、はて?(書き始めてから時間が経ってしまい、意味がわからなくなったのでゆっくり行きます。)
Reactとは?のところにこうありました。
コンポーネントは props(“properties” の略)と呼ばれるパラメータを受け取り、render メソッドを通じて、表示するビューの階層構造を返します。
上の階層から渡ってきたpropsはrenderメソッド内で
上の階層で設定された値を返すことができるよ
ってことですね。
最初のデータをprops経由で渡すでは、
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />;
}
}
Boardの rederSquare
メソッドで value={i}
を返すように定義すると、
Squareの方で、 {this.props.value}
としたときに value
の値の{i}が渡ってくる。
ということでした。ふむふむ。
そういうわけで、今度は、Boardのrenderメソッド(の中で使われているrenderSquareメソッド)で
Gameコンポーネントで設定したsquaresの値をpropsとして受け取って使うようにしたいので、
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.state.squares[i]} ↓に書き換え
value={this.props.squares[i]}
// onClick={() => this.handleClick(i)} ↓に書き換え
onClick={() => this.props.onClick(i)}
/>
);
}
とpropsを受け取って返すように書き換えるわけです。
ん??
handleClick(i)
がどうしてprops.onClick(i)
になるの・・・??
handleClick
は 元々Boardコンポーネントで定義していたメソッドでした。
そしてメソッドの中身を見てみると、Gameコンポーネントで扱うようにした、
squareのステータスを扱っているメソッドなので、どうやらこのメソッドもBoardコンポーネントからGameコンポーネントに移した方が良さそうです。
というわけで、後々、handelClick
メソッドもGameコンポーネントに移すことになるので、
SquareコンポーネントからBoardコンポーネントにリフトアップした時と同様に this.props.onClick(i)
として、
Gameコンポーネントから渡されたonClick
プロパティを受け取るようにします。
Gameコンポーネントのrenderの更新
続きまして、
Game コンポーネントの render 関数を更新して、ゲームのステータステキストの決定や表示の際に最新の履歴が使われるようにします。
です。
ここはもう見たまんまかな、という感じ。
render() {
const history = this.state.history;
//historyはGameコンポーネントで扱うようになったので 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)}
//handleClickは後でGameコンポーネントに持ってくるので this.handleClick(i)
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
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をGameコンポーネントに移し、内容もGameコンポーネントに対応させます。
handleClick(i) {
const history = this.state.history; //現在の手番の履歴を設定
const current = history[history.length - 1];
//現在の手番の状態を設定 ("historyの履歴の数-1"番目のsquaresを取ってくる)
// -1してるのは、配列は0番目から数えるため
const squares = current.squares.slice(); //現在のsquaresを複製
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{ //手番の履歴に今の盤の状態を追加
squares: squares,
}]),
xIsNext: !this.state.xIsNext,
});
}
ここまでで、過去の履歴が保存できるようになり、保存した履歴はGameコンポーネントで扱えるようになっています。
過去の着手の表示
Gameコンポーネントで履歴を扱うようになったので、履歴を表示する準備ができました。
Gameコンポーネントのrender内に履歴を表示させていきます!
その前にmapメソッドについておさらい。
配列の中身を一個一個取り出して、処理を加え、再配列するメソッド。
(ちなみに動詞mapには 〈関数・集合〉を変換する.
という意味があります。(そのまんま))
引数を2つ取るときは、一つ目が値、二つ目がindexになります。
(3つ目を取るとき、3つ目は現在処理している配列そのもの)
では早速変更を加えたrender内を見ていきます。
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
//履歴を一個一個取り出して、再配列、movesという名前で過去の手番たちを定義。
const desc = move ? //move(手番のindex)が存在するかどうかで条件分岐してdescに代入
'Go to move #' + move : //moveが存在するとき descに'Go to move #' + move(手番のindex)という文字列を代入
'Go to game start'; //moveが存在しないときは descに'Go to game start'という文字列を代入
return (
<li> //取り出された値一つ一つをリスト(ボタン)で表示
<button onClick={() => this.jumpTo(move)}>{desc}</button>
//ボタンには jumpTo(move) という動きをつけておく
//ボタンには↑で定義したdescの文字列を表示させる
</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>
);
}
# Keyを選ぶ
小難しいことがいっぱい書いてあるのでかいつまんでみます。
Keyとは!
Reactがリストをレンダーするとき、リストアイテム一つ一つを固有のものとして識別するために必要なid的なもの。
keyを指定していないと、Reactは警告を出しつつ、配列のindexをkeyとして使用してくれますが、配列のインデックスをkeyとして使うことは諸々の問題の原因になるので、keyは指定しましょう!
今回は着手の手番がkeyとして使えるので、リストアイテムのkeyとして指定します。
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>
);
});
続いて、今何手目の状態を見ているのかを表すため、GameコンポーネントのstateにstepNumberという値を加えます。
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0, // ←ここ 初期値は 0
xIsNext: true,
};
}
それではjumpToメソッドを定義していきます。
//Gameコンポーネント内
jumpTo(step) { //renderメソッド内で使う際には引数にmoveを渡してますね
this.setState({ //↑で加えたstepNumberというstateの値をセットします
stepNumber: step, //セットするのは引数で渡ってきたmove(手番)
xIsNext: (step % 2) === 0, //手番を2で割ってあまりが0になるかどうか(手番が偶数か奇数かで true or falseを値としてセット
});
}
これで、押したボタンの手番通りの番号が、stepNumberというstateにセットされるようになりました。
ただしこれだけだと、新しくマス目をクリックしたときはstepNumberの値がまだセットされるようになってないので、そこのところをやっていきます。
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
//↑書き換え 履歴の先頭(0)から既存の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, //← 追記 マス目をクリックしたときに、stepNumberの値としてhistory.length(履歴の長さを取ることで何番目の手番かをセットできる
xIsNext: !this.state.xIsNext,
});
}
これで、jumpボタンを押した後も綺麗に履歴が書き変わっていくようになりました。
いよいよ次で最後です!
現状、Gameコンポーネントのrenderメソッドでは
render() {
const history = this.state.history;
const current = history[history.length - 1]; //← ここ
と、常に最新の履歴を表示するようになっていますが、これだとタイムトラベルボタンを押す上で都合が悪いので、
ここは現在選択している手番の状態が見れるようにしたいですよね。
ということで、ここを
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
こうして、現在のstepNumber番目の履歴を取ってくるようにすればOKです!!!
これにて三目並べゲームの完成です!!!!!わーーい
編集後記(大袈裟)
この記事を書き始めてから、ちょっと脱線してたり、
娘のイヤイヤ期&夜泣き復活&感染症で保育園が休園したりで体力の限界を迎えたり(言い訳ですが。。)で、
びっくりするほどの月日が経過しておりました・・・。
そして会社の方からこの三目並べあまりに内容が古いときいて
こんな月日かける必要があったのかというのも疑問に感じたりしたんですが
やらないよりはマシだという気持ちでなんとか書き切りました。
次は一旦追加課題は置いておいて、積読していた、
[モダンJavaScriptの基本から始める React実践の教科書] (https://www.amazon.co.jp/dp/481561072X/ref=cm_sw_r_tw_dp_Z854CPEP5TDDBV81MJK1) に進もうと思います!