WebフレームワークであるReactについて、実際に手を動かしながら学んでいきたいと思います。
1. Create React Appでプロジェクトの雛形を作成
2. Reactについて学ぶ
3. 三目並べを関数コンポーネントで実装する ← ここ
作成するアプリケーション
React公式のチュートリアルで実装する三目並べを実装します。
チュートリアルではクラスコンポーネントで実装されていますが、本稿では関数コンポーネントだけを用いて実装します。
通常の三目並べに加え、特定の手数まで戻るタイムトラベル機能がついています。
コンポーネントの構成
下記のようなコンポーネントの構成で実装していきます。
GameコンポーネントはBoardコンポーネントを子の要素として持ち、Boardコンポーネントは9個のSquareコンポーネントを子として持ちます。
単純な三目並べの実装(Boardまで)
まずはタイムトラベル機能を持たない、単純な三目並べを実装していきます。
Square コンポーネント
9個ある正方形の升目をボタンとして実装します。
props
をBoardから受け取り、props.value
を表示、クリック時にはprops.onClick
をコールするようにします。
import './App.css'; // スタイル適用のためにcssをインポート
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
スタイルを適用させるため、App.cssをApp.jsと同じフォルダに作成し、下記のように記載します。
.square{
height: 50px;
width: 50px;
vertical-align: top;
}
Board コンポーネント
[1/3] Squareを並べてみる
まずはSquareコンポーネントを3×3に並べてプロパティを設定してみましょう。
<Square/>
を一つ一つ<div>
の中に書いてもいいのですが、見通しと保守性を考えて、value
を引数に持つrenderSquare
でSquareコンポーネントを作成するようにします。
この時にvalue
とonClick
をpropsとして渡しているのがわかりますね。
function Board(){
function handleClick(value){
alert(value);
};
function renderSquare(value){
return(
<Square value={value} onClick={()=>handleClick(value)}/>
);
};
return(
<div>
<div>
{renderSquare(0)}
{renderSquare(1)}
{renderSquare(2)}
</div>
<div>
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</div>
<div>
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</div>
</div>
)
}
export default Board
下記のように3×3のボタンが表示され、ボタンをクリックするとボタンと同じ番号がalertされるのが確認できます。
[2/3] stateを実装する
三目並べとしたいので、Squareの中に書かれている内容は数字ではなくOかXかを表示させる必要があります。
そのために、現在の盤面を下記のように配列で表現し、これをstateとして保存するようにします。
var squares = [ null, null, 'X'
'X' , 'O' , null
null, null, null]
また、手番を交代とするために、xIsNext
というstateも持つようにします。
まずはstateを利用するために、useState
をインポートします。
+ import React, {useState} from 'react';
import './App.css';
handleClick
、renderSquare
を下記のように更新することで、ボタンをクリックすると交代でXとOが入力されるようになります。
handleClick
内でsquares
をそのまま使わずにコピーを取っている理由については、Reactのチュートリアルを参照してください。
import React, {useState} from 'react' // useState利用のためにimportする
// function Square()...
function Board(){
const [squares, setSquares] = useState(Array(9).fill(null)); // 盤面
const [xIsNext, setXIsNext] = useState(true); // 手番
function handleClick(value){
const newSquares = squares.slice(); // squaresのコピーを取得
newSquares[value] = xIsNext? 'X' : 'O'; // 手番に応じてXかOを設定
setSquares(newSquares); // squaresを更新
setXIsNext(!xIsNext); // 手番を更新
};
function renderSquare(value){
return(
<Square value={squares[value]} onClick={()=>handleClick(value)}/> // valueとしてsquares[value]を渡すように変更
);
};
//return(...
}
[3/3] 勝敗の判定を実装する
盤面の状態に応じて、手番の表示と勝敗の判定を行う部分を実装していきます。
まずは手番を表示できるようにします。
メッセージを表すstatus
stateを定義し、それを表示する<div>
タグを記述します。
function Board(props){
const [squares, setSquares] = useState(Array(9).fill(null));
const [xIsNext, setXIsNext] = useState(true);
const [status, setStatus] = useState("");
// function renderSquare(value){
// ...
// };
return(
<div>
<div>{status}</div> {/*追加*/}
<div>
{/*renderSquare(...)*/}
</div>
</div>
)
}
このstatusを手番ごとに更新するのですが、ここではuseEffect
というHookを利用します。
useEffect
はViewが更新される度にコールされ、ここでstatus
メッセージを更新することにします。
useEffect
を利用するために、reactからuseEffect
をインポートしておきます。
- import React, {useState} from 'react';
+ import React, {useState, useEffect} from 'react';
import './App.css';
勝敗を判定する関数は公式のものをコピーしてきます。
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;
}
useEffect
を実装していきます。calculateWinner
がnull
でない場合はstatus
に勝者を表示、そうでない場合は手番を表示します。
function Board(props){
// ...
// function renderSquare(value){
// ...
// };
useEffect(()=>{
const winner = calculateWinner(squares);
if(winner){
setStatus("Winner : " + winner);
}
else{
setStatus('Next player: ' + (xIsNext ? 'X' : 'O'));
}
})
// return(...
}
このままだと、ゲーム終了後にも盤面をクリックできてしまう&同じボタンが何度も押せてしまうので、下記のようにhandleClick
を修正します。
function handleClick(value){
if (calculateWinner(squares) || squares[value]) {
return;
}
// ...
};
App.js全体
App.jsの全体は下記のようになります。
import React, {useState, useEffect} from 'react';
import './App.css';
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
function Board(props){
const [squares, setSquares] = useState(Array(9).fill(null));
const [xIsNext, setXIsNext] = useState(true);
const [status, setStatus] = useState("");
function handleClick(value){
if (calculateWinner(squares) || squares[value]) {
return;
}
const newSquares = squares.slice();
newSquares[value] = xIsNext? 'X' : 'O';
setSquares(newSquares);
setXIsNext(!xIsNext);
};
function renderSquare(value){
return(
<Square value={squares[value]} onClick={()=>handleClick(value)}/>
);
};
useEffect(()=>{
const winner = calculateWinner(squares);
if(winner){
setStatus("Winner : " + winner);
}
else{
setStatus('Next player: ' + (xIsNext ? 'X' : 'O'));
}
})
return(
<div>
<div>{status}</div>
<div>
{renderSquare(0)}
{renderSquare(1)}
{renderSquare(2)}
</div>
<div>
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</div>
<div>
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</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;
}
export default Board;
タイムトラベル機能の実装(Gameの実装)
ここで完成形となるGameコンポーネントを実装します。
今まで作成したBoardコンポーネントを子要素に持たせ、今までの三目並べの機能に加えて、任意の手番まで戻ることができるタイムトラベル機能を実装します。
タイムトラベル機能の実現のため、過去の手番すべてを記憶させた、history
配列を用意します。
var history = [
{
squares: [ null, null, null
null, null, null
null, null, null]
},
{
squares: [ null, null, null
null, 'X' , null
null, null, null]
},
{
squares: [ null, null, 'O'
null, 'X' , null
null, null, null]
},
// ...
[1/3] stateのリフトアップ
上記のhistory
というstateはゲーム全体に関わる要素なので、Gameコンポーネントのstateとして保持することにします。
こうすることで、Boardコンポーネントはsquares
というstateを保つ必要がなくなり、親要素であるGameコンポーネントから逐一squares
を受け取り、それを描画するだけでよくなります。
これに合わせて、stateを変更するhandleClick
や、他のstate(xIsNext
、status
)もGameコンポーネントにリフトアップさせます。
function Game(){
const [history, setHistory] = useState([{squares: Array(9).fill(null)}]);
const [xIsNext, setXIsNext] = useState(true);
const [status, setStatus] = useState("");
function handleClick(value){
const current = history[history.length - 1];
if (calculateWinner(current.squares) || current.squares[value]) {
return;
}
const newSquares = current.squares.slice();
newSquares[value] = xIsNext? 'X' : 'O';
setXIsNext(!xIsNext);
setHistory(history.concat([{squares: newSquares}]));
};
useEffect(()=>{
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
if(winner){
setStatus("Winner : " + winner);
}
else{
setStatus('Next player: ' + (xIsNext ? 'X' : 'O'));
}
});
return(
<div>
<div>
<Board squares={history[history.length - 1].squares} onClick={(i)=>handleClick(i)}/>
</div>
<div>
<div>{ status }</div>
</div>
</div>
);
}
Boardコンポーネントはstateを扱う必要がなくなり、親要素からpropsとして受け取った値だけを表示するだけでよくなります。
function Board(props){
function renderSquare(value){
return(
<Square value={props.squares[value]} onClick={()=>props.onClick(value)}/>
);
};
return(
<div>
<div>
{renderSquare(0)}
{renderSquare(1)}
{renderSquare(2)}
</div>
<div>
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</div>
<div>
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</div>
</div>
)
}
[2/3] 過去の手の表示
history
に対してmap
を行い、その結果をmoves
として表示させるようにします。
ループでリストの要素を追加しているため、key要素を割り当てる必要があります。詳細はこちら。
ボタンをクリックした際にコールされるjumpTo
は後ほど実装します。
function Game(){
// ...
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}> {/*リストにはKeyを割り当てる必要がある*/}
<button onClick={() => jumpTo(move)}>{desc}</button> {/*jumpToは後ほど実装*/}
</li>
);
});
// ...
return(
<div className="game">
<div className="game-board">
<Board squares={history[stepNumber].squares} onClick={(i)=>handleClick(i)}/>
</div>
<div className="game-info">
<div>{ status }</div>
<ol>{moves}</ol> {/*追加*/}
</div>
</div>
);
}
[3/3] タイムトラベルの実装
タイムトラベル機能の実装にあたって、現在の手が何番目かを表すstepNumber
というstateを定義します。
function Game(){
const [history, setHistory] = useState([{squares: Array(9).fill(null)}]);
const [xIsNext, setXIsNext] = useState(true);
const [status, setStatus] = useState("");
+ const [stepNumber, setStepNumber] = useState(0);
// ...
}
このstepNumber
は手番が進むたびに更新される必要があるため、handleClick
の中で更新されるようにします。
また、それに合わせて現在の盤面を表すcurrent
をstepNumber
から取得するように変更します。
function Game(){
// ...
function handleClick(value){
+ const hist = history.slice(0, stepNumber + 1);
+ const current = hist[hist.length - 1];
- const current = history[history.length - 1];
if (calculateWinner(current.squares) || current.squares[value]) {
return;
}
const newSquares = current.squares.slice();
newSquares[value] = xIsNext? 'X' : 'O';
setXIsNext(!xIsNext);
+ setStepNumber(hist.length);
setHistory(hist.concat([{squares: newSquares}]));
};
useEffect(()=>{
+ const current = history[stepNumber];
- const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
if(winner){
setStatus("Winner : " + winner);
}
else{
setStatus('Next player: ' + (xIsNext ? 'X' : 'O'));
}
});
// ...
}
最後に、未定義だったjumpTo
メソッドを下記のように定義して完成となります。
function Game(){
// ...
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0,
});
}
// ...
}
お疲れさまでした!これで三目並べの関数コンポーネントでの実装は完了です!
最後に、App.js全体のコードを貼り付けておきます。
import React, {useState, useEffect} from 'react';
import './App.css';
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
function Board(props){
function renderSquare(value){
return(
<Square value={props.squares[value]} onClick={()=>props.onClick(value)}/>
);
};
return(
<div>
<div>
{renderSquare(0)}
{renderSquare(1)}
{renderSquare(2)}
</div>
<div>
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</div>
<div>
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</div>
</div>
)
}
function Game(){
const [history, setHistory] = useState([{squares: Array(9).fill(null)}]);
const [xIsNext, setXIsNext] = useState(true);
const [status, setStatus] = useState("");
const [stepNumber, setStepNumber] = useState(0);
function jumpTo(step){
setStepNumber(step);
setXIsNext((step%2) === 0);
}
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{desc}</button>
</li>
);
});
function handleClick(value){
const hist = history.slice(0, stepNumber + 1);
const current = hist[hist.length - 1];
if (calculateWinner(current.squares) || current.squares[value]) {
return;
}
const newSquares = current.squares.slice();
newSquares[value] = xIsNext? 'X' : 'O';
setXIsNext(!xIsNext);
setStepNumber(hist.length);
setHistory(hist.concat([{squares: newSquares}]));
};
useEffect(()=>{
const current = history[stepNumber];
const winner = calculateWinner(current.squares);
if(winner){
setStatus("Winner : " + winner);
}
else{
setStatus('Next player: ' + (xIsNext ? 'X' : 'O'));
}
});
return(
<div className="game">
<div className="game-board">
<Board squares={history[stepNumber].squares} onClick={(i)=>handleClick(i)}/>
</div>
<div className="game-info">
<div>{ status }</div>
<ol>{moves}</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;
}
export default Game;