この記事はPart1の続きである.
まず下記リンクを参照されたい.
Reactチュートリアル
三目並べを完成させる
ここまでで基本的な部品が揃った.これらの部品を使用して,三目並べを完成させる.
9マスの状態を管理する機能を作成する.
現在,各マス目の状態(値)はSquare
コンポーネントが保持している.最終的にこのゲームの勝者を判定する必要があり,9マスの情報を管理する機能が必要となる.
まず,マス目の管理用はArray
の定義とする.renderSquare
が定義したArray
から値を読み込むように変更する.
- 2-7行目
9マスの値を保存するArray
を定義 - 10行目
Array
から値を読み込むように変更
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]} />;
}
...
}
マス目がクリックされたときに,マス目を管理するBoard
コンポーネントのArrayの値を変更する必要がある.Board
コンポーネントのstate
はプライベートなため,Square
からBorad
のstate
の値を変更することができない.
そのため,Board
からSquare
にhandleClick
を通じて値を渡す.
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)}
/>
);
}
...
}
現在のコードでは,Board
からSquare
にvalue
とonClick
の2つのprops
を渡している.
ここで,Square
プロパティに以下のような変更を加える.
Square
コンポーネントのthis.state.value
をthis.prop.value
に変更する.Square
コンポーネントのthis.setState()
をthis.prop.onClick()
に変更する.Square
コンポーネントはもはやstate
を必要としないため,constructor
を削除する
class Square extends React.Component {
render() {
return (
<button
className="square"
onClick={() => this.props.onClick()}
>
{this.props.value}
</button>
);
}
}
Board
コンポーネントがマス目の値を管理し,Square
コンポーネントのstate
を読み込まないため,が不要となった.
現時点でのコードに関してまとめると
- DOMコンポーネント(ReactDOM)の
<button>
にonClick
- ボタンがクリックされると
Square
内のrender()
メソッドに定義されているonClick
が呼ばれる. - このイベントハンドラが,
this.props.onClick()
を呼び出す. Board
はSquare
にonClick={() => this.handleClick(i)}
を定義しているので,Square
がクリックされたときにthis.handleClick(i)
を呼び出す.this.handleClick(i)
に関して,現時点では定義していないためクラッシュする.
次に,handleClick(i)
を定義する.
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[i] = "X";
this.setState({ squares: squares });
}
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
ここまでの変更で,「Squareコンポーネントがsteteを管理する」状態から「Boardコンポーネントがstateを管理し,SquareコンポーネントはクリックされたことをBoardに伝える」という構造になった.
この状態のSquare
コンポーネントのことを,React用語では’Controlled component’と呼ぶらしい.
現時点のコードは以下のようである.
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
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)
};
}
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)}
/>
);
}
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("root"));
onClick
をmyOnClick
として変更したコードを以下に示す.これは,上のコードと全く同じ挙動をするはずである.
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class Square extends React.Component {
render() {
return (
<button className="square" onClick={() => this.props.myOnClick()}>
{this.props.value}
</button>
);
}
}
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[i] = "X";
this.setState({ squares: squares });
}
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
myOnClick={() => this.handleClick(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 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("root"));
Squareクラスコンポーネントを関数コンポーネントに変更
Square
クラスコンポーネントはstate
を持たず,render
メソッドのみを有するため関数コンポーネントに変更することが可能である.関数コンポーネントに利点は,よりシンプルに記述できることである.
class Square extends React.Component {...}
を以下のように変更する.
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
“X”, “O”が交互に入力されるようにする
現状のコードは,”X”のみが表示される.本来であれば”X”と”O”が交互に表示されなければおかしい.表示を制御するための変数xIsNext
をstate
に定義する.
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
...
}
Square
がクリックされたとき,xIsNext
の状態から”X”, “O”を判定し,xIsNext
を更新する.
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? "X" : "O";
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext
});
}
さらに”X”に”O”の番手によってPlayer情報を書き出すようにする.
render() {
const status = "Next player: " + (this.state.xIsNext ? "X" : "O");
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
現状は以下のようなアプリケーションになっている.
現時点のコードは以下のようである.
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
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
};
}
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>
);
}
}
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("root"));
勝敗の判定
以下のような勝敗を判定するための関数をファイルに追記する.
この関数は,「現在の盤面に勝者がいるか確認し,勝者がいる場合”X”もしくは”O”を返し,いない場合nullを返す」ものである.
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;
}
この関数をBoard
コンポーネントのRender
で呼び出し,現在の盤面の情報を表示する.
- 勝者がいる場合,「Winner : “X”」 もしくは 「Winner : “O”」を表示
- 勝者がいない場合,「Next player : “X”」もしくは「Next player : “O”」
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">
現コードでは,マス目に”X”もしくは”O”が入力されている場合でも,上書き入力できてしまうため,以下のように変更する
- すでに入力がある場合上書きできないようにする.
- 勝者がいる場合でも入力できないようにする
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
});
}
現時点でのコード
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
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
};
}
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>
);
}
}
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>
);
}
}
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;
}
// ========================================
ReactDOM.render(<Game />, document.getElementById("root"));
「戻る」機能を実装する
記事が長くなってきたので,別ページに記載する.
下記リンクを参照されたい.
最後に
内容に誤りや不具合,ご意見があればコメントを残して頂けるとありがたいです
コメント