はじめに
React公式チュートリアルを完了したので、アウトプット&初学者の方に向けた解説を目的として記事にしました。
実際の手順に関しては公式に細かく記載されているため、本記事では実装手順を省き、内部の動きについてかなり細かく噛み砕いて解説しました。
対象としては、
- 何となくは理解できたけど細かい所が不安・・・
- とりあえずチュートリアルを終わらせたけど何が起こっているのかよく分からん
- そもそもJavaScriptが分からねえ
上記のような方々を対象としています。
少し長めの記事ですが、本記事を読めば公式チュートリアルの内容をほぼ理解できるような内容になっているかと思いますので、最後までどうかお付き合い下さい。
Squareコンポーネント
ここでは、三目並べのマス目部分を担当しているSquareコンポーネントについて解説します。
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
とは言っても、非常に単純な構成のため1点のみです。
1. クラスコンポーネントではなく関数コンポーネントを使用
チュートリアルのコンポーネント構成は、上からGame > Borad > Squareとなっており、stateは全てGameコンポーネントで管理されています。
そのため、単に要素をreturnするだけのSquareコンポーネントは、よりシンプルに記載できる関数コンポーネントで実装されています。
Boardコンポーネント
Boardコンポーネントは、Squareのまとまりを管理しています。
render内に記載されている9つのrenderSquare関数によって、Boradコンポーネント内にSquareコンポーネントを配置しています。
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(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>
);
}
}
先程と比べると少しだけコード量が増えてきましたね。
しかしここではrenderSquare関数を押さえれば意味が理解できると思います。
1. Square value
{this.props.squares[i]}のsquaresは、Gameコンポーネントで管理されている配列のstateです。
後述しますが、squaresは9つのマス目全てに関して、どこにどちらのマーク(☓か○か)が入っているのか情報を持っています。
引数として渡す数値をもとに、マス目の場所に応じたマークを呼び出し、Square毎のvalueに代入しています。
2. Square onClick
ここでは、Gameコンポーネント内のhandleClick関数を呼び出すための処理が記載されています。
Gameコンポーネントのrender部分は以下です。
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
Boardコンポーネント内のonClick={() => this.props.onClick(i)}によって、配置を表す引数を渡しつつhandleClick関数を呼び出しています。
この記述をもとに、マス目毎にhandleClickイベントを発動させることができるようになっています。
3. onClick内の() =>
onClick={() => this.props.onClick(i)}の中に記載されているアロー関数は何のためにあるのか?
答えはクラスメソッドをバインドするためです。
JSX のコールバックにおける this の意味に注意しなければなりません。JavaScript では、クラスのメソッドはデフォルトではバインドされません。this.handleClick へのバインドを忘れて onClick に渡した場合、実際に関数が呼ばれた時に this は undefined となってしまいます。
公式ドキュメント:イベント処理
つまり、今回のように末尾に()を付けずに関数を呼び出す際は、何かしらの形でバインドしなければなりません。
別の方法としては、Gameコンポーネントのコンストラクタ内に以下のように記載することで解決できます。
this.handleClick = this.handleClick.bind(this);
calculateWinnerコンポーネント
Gameコンポーネントに進む前に、まずは「勝利条件が成立しているか」を判定するcalculateWinnerコンポーネントについて、ほぼReactに関係ない部分ですが説明しておきます。
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;
}
1. lines定数
蛇足ですが、三目並べの勝利条件は「縦・横・斜めのいずれかに同じマークが3つ並ぶこと」です。
後述するfor文で、lines内の組み合わせ全てに、同じマークがあるかどうかを判定していきます。
ハードコーディングされていますが、今回は3マス×3マスかつ三目並べ専用なので問題はありません。
2. for文
まずconst [a, b, c] = lines[i]で、lines内の組み合わせを分割代入しています。
例えばlines[2]であれば、a, b, cはそれぞれ6, 7, 8が代入されます。
それらを9つのマス目全ての情報を持っているsquares配列のインデックスとして使用し、if文内で総当たりの条件判定を行っている形です。
Gameコンポーネント
いよいよ本題のGameコンポーネントです。大まかに機能を分けると、
1. state
2. handleClick
3. jumpTo
4. render
このあたりでしょうか。順を追って解説していきます。
コード全体
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
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() {
const history = this.state.history;
const current = history[this.state.stepNumber];
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>
);
});
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>
);
}
}
// ========================================
ReactDOM.render(
<Game />,
document.getElementById('root')
);
1. state
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
xIsNext: true,
};
}
-
history
ここではhistory内部に配列のsquaresがセットされています。
まずArray(9).fill(null)は「全てがnullである、サイズが9の配列」です。
この記述によりstateの初期値として、一切マークされていない、ゲームスタート時のボードがセットされます。
次に直接suqaresを保持するのではなく、わざわざhistory内部にsquaresを保持している理由ですが、これは後で過去の着手を表示するためです。
後述しますが、ゲームが進行する度に1つずつvalueが追加されたsquaresが増えていくイメージ。
-
stepNumber
render内にconst current = history[this.state.stepNumber];と記載されている通り、ゲームの手番を管理するためのstateです。
history[]にインデックスを渡すことで、現在のsquaresの状態をcurrentに代入しています。 -
xIsNext
こちらは次の手番が○か☓かを管理するためのstateです。
同じくrender内にstatus = 'Next Player: ' + (this.state.xIsNext ? 'X' : 'O');と記載されています。
見慣れない方も居るかもしれませんが、こちらは条件(三項)演算子と呼ばれる記法で、?までが条件文、:の左がtrue、右がfalse時の処理を表しています。
今回の例だとthis.state.xIsNextがtrueならX、falseなら○と、より簡潔に記述できますね。
2. handleClick
ここではイベント発火元Squareのインデックスを引数として、squareがクリックされた時の処理について解説します。
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,
});
}
- 定数
-
history
現在のhistoryをsliceメソッドで「現在の手番 + 1」で作り直し、historyに代入しています。
ぱっと見は何のための処理か分からなくなりそうですが、これは後述のjumpToメソッドが実行された時に必要な処理です。
jumpToメソッド内ではhistoryをsetStateしないので、メソッド実行後もhistoryの中身は変わりません。
しかし、このままではhistoryがおかしくなるため、handleClickメソッド実行時に正しい中身に再設定している形です。
以下は前回画像の状況から過去の手番に戻った場面ですが、盤面とhistoryの数が一致していません。
空のマス目をクリックすることで`historyが作り直され、正しい履歴に戻ります。
-
current
こちらは単に現在のhistoryのみを取得・代入しています。 -
squares
cconst squares = current.squares.slice()と繋げることで、盤面を更新する前に、現在の状態をsquaresに代入しています。
- 処理
- if文
ここでは「ゲームの勝者が居る場合」 or 「マーク済のマスがクリックされた場合」に早期returnするためにif文が使用されています。
if (calculateWinner(squares) || squares[i])となっているので、どちらかがtrueであればこの先の処理は行われません。 - 条件演算子
先程のような構文が再び出てきましたね。ここではxIsNextがtrueなら☓、falseなら○がクリックされたsquareのvalueとして代入されます。
- setState
-
history
少し前でhistoryを作り直した意味がやっとここで出てきます。
history.concat([{squares: squares,}])とすることで、今までの履歴に今回作成されたsquaresを追加できます。
この一連の処理によって、今までの履歴を管理しつつ正しい順番で今回の処理結果を追加することができました。 -
stepNumber
どうせ使用時に+ 1するので単にhistory.lengthを代入。 -
xIsNext
!this.state.xIsNextとする事で値を反転させています。
xIsNextは真偽値なので、この処理の度にtrue / false切り替わる形です。
3. jumpTo
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0,
});
}
-
stepNumber
引数のstepにはターゲットとなるhistoryのインデックスが入るため、stepNumberに正しい値がセットされます。 -
xIsNext
xIsNext: (step % 2) === 0とすることでxIsNextに正しい値がセットされます。
4. render
いよいよ大詰めです。少し長いですが頑張りましょう!
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
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>
);
});
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>
);
}
- 定数
-
const history = this.state.history
現時点での全てのhistoryをセット -
const current = history[this.state.stepNumber]
最新の盤面をセット -
const winner = calculateWinner(current.squares)
現在の盤面での勝者をセット -
const moves = history.map((step, move) => {}
まず、この処理内で使われているdescは「descending order」ではなく「description」です。(蛇足かもしれませんが、初見で勘違いしたので・・・)
ここではhistoryそれぞれのインデックスを取得するためにmap処理を行っています。
なので「'step' が宣言されていますが、その値が読み取られることはありません。」と表示されていますが問題ありません。
const desc = move ? 'Go to move #' + move : 'Go to game start';
ここでまたもや条件演算子の登場です。
moveがtrue、つまり「0やnull以外の値」であればdescに'Go to move #' + move'を代入、moveがfalseであれば'Go to game start'を行います。
moveがfalse(今回だと0のみ)の場合はhistory[0]=ゲームスタート時の盤面なので'Go to game start'と表示させる寸法です。
-
let status
空のstatusを宣言しておき、if文内でwinnerの有無に応じて表示を出し分けています。
ここは非常にシンプルですね。 -
return
ここではあまり説明することもありませんが1点だけ。
onClick={(i) => this.handleClick(i)}でアロー関数の引数になっているiですが、これはBoardコンポーネント内のrenderSquare(i)から来ています。
Square valueに渡しているインデックスをついでに利用した形です。
あとは最後にReactDOM.renderして終了です!
おわりに
ここまでお付き合い下さりありがとうございました。
本記事が少しでも皆様のお役に立てれば幸いです。
ほぼ初めての記事投稿だったのですが、作成にあたり自分の中でも理解が深まったと感じます。
散々言われている事ですが、やはり自身で言語化 → アウトプットする事は大切だなーとしみじみ・・・
これを機にアウトプットをしっかり癖付けていきます。
、、、とはいえ今年は終わりが近いので来年から頑張ることにします。
それでは皆さま良いお年を。



Comments
Let's comment your feelings that are more than good