前回、こちらの記事でReactの公式チュートリアルを解説しました。
公式のチュートリアルでは、最後に追加課題が出されています。
今回はその追加課題をやっていこうと思います。
Contents
チュートリアル本編で作ったもの
公式のReactチュートリアルで、このようなものを作成しました。
誰でもやってことがある「○×ゲーム」です。公式ページではtic-tac-toeゲーム(三目並べ)と呼ばれています。
機能としては
- プレイヤーが交互に順番が回ってくる
- すでに埋まったマスは埋めれない
- 1列揃った時点で終了
- 上に現状のプレイヤーが誰か、勝負がついた場合に勝者が通知される
- 右に履歴がでてきてボタンを押したらその時点に遡ることができる。
というようなものです。
本家の完成版がこちらにのっています。
https://github.com/fkt1993/react-sample-app
前回作成したものはこちらのリポジトリにまとめましたのでそちらでも確認ができます。
今回扱う内容もこちらのリポジトリに追加していきます。
追加課題の内容
公式のチュートリアルではこのように出されています。
1.Display the location for each move in the format (col, row) in the move history list.
2.Bold the currently selected item in the move list.
3.Rewrite Board to use two loops to make the squares instead of hardcoding them.
4.Add a toggle button that lets you sort the moves in either ascending or descending order.
5.When someone wins, highlight the three squares that caused the win.
6.When no one wins, display a message about the result being a draw.
ということなので、日本語で訳すと
1. それぞれの遷移に(col,row)の表示をする
2. 遷移リストの現在選択されているものを太字に
3. Boardをloop2回つかって書いてみる
4. 遷移リストを昇順/降順にする
5. 誰かが勝った時に、3つ揃ったセルをハイライトする
6. 引き分けの時にはdrawを表示
こんなかんじになります。
では1つずつ実装していきましょう。
実際にやってみる
課題1.それぞれの遷移に(col,row)の表示をする
右側に履歴が表示されているかと思いますが、その1つ1つの履歴に現状は「#1」というような表示だったのを「#1(0,1)」のようにどのマスを選択したかがわかるように表示していきます。
この履歴に関してはGameコンポーネント
のhistory
に格納されている配列を、map関数
で表示しています。
なのでこのhistory
にクリックしたマスの情報も加えて格納するように変更していきます。
マスをクリックした際に呼ばれるのが、Gameコンポーネント
のhandleClickメソッド
で、こちらでhistory state
を書き換えているので、handleClickメソッド
を書き換えます。
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,
col: (i % 3) + 1, //追加
row: Math.floor(i / 3) + 1, //追加
}]),
xIsNext: !this.state.xIsNext,
stepNumber: history.length
});
}
history
に新しい配列の要素を追加する際にcol
とrow
を追加するようにしています。
これをもとにhistory
の内容をレンダリングする部分で、この値を使ってマスの位置を表示するように書き換えます。
Gameコンポーネント
のrenderメソッド
内でhistory
に対してmap関数
を通していたのでその部分を書き換えました。
const moves = history.map((step, move) => {
const desc = move ?
'Move #' + move + '(' + step.col + ',' + step.row + ')': //変更
'Game start';
return (
<li key={move}>
<button onclick="{()"> this.jumpTo(move)}>{desc}
</li>
);
});
以上です。実際に確認してみましょう。
↓このように座標も表示されるようになりました。
これで1番目の課題は完了です!
課題2. 遷移リストの現在選択されているものを太字に
方針としては、Gameコンポーネント
にstepNumber
をステートとして保持しているので、これを利用してstepNumber
とhistory
のindex
の役割をするmove
が一致した際にbold
というclass
を付与します。
const moves = history.map((step, move) => {
const desc = move ?
'Move #' + move + '(' + step.col + ',' + step.row + ')':
'Game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)} className={this.state.stepNumber === move ? 'bold' : '' } >{desc}</button> //変更
</li>
);
});
boldクラス
がついたときに文字が太字になるようにcss
も編集します。
.bold {
font-weight: bold;
}
以上です。実際に確認してみましょう。
↓このように現在選択されているものが太字になりました。
これで2番目の課題は完了です!
課題3. Boardをloopを2回つかって書いてみる
現状Boardコンポーネント
のrenderメソッド
では以下のようにハードコーディングされています。
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>
);
}
}
課題3では、こちらをloop
を2回使って書き換えていきます。
具体的にはmap関数
を2重で使っていきます。
以下のように書き換えました。
class Board extends React.Component {
renderSquare(i) {
return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} key={i}/>; //keyを追加
}
render() {
return (
<div>
{
Array(3).fill(0).map((row, i) => {
return (
<div className="board-row" key={i}> //keyを追加
{
Array(3).fill(0).map((col, j) => {
return(
this.renderSquare(i * 3 + j)
)
})
}
</div>
)
})
}
</div>
);
}
}
ポイントとしてはmap関数
を使うときはkey
を加えることです。
なぜkey
を使うかというのは、こちらの記事でまとめているのでわからない場合は参考にしてみてください。
以上で課題3が完成です!
遷移リストを昇順/降順にする
遷移リストの上にボタンを設置して、クリックすると遷移リストが昇順か降順か切り替わるようにしていきます。
方針としては、新たに昇順か降順かどうかを保持するstate
を新しく定義して、遷移リストをレンダリングする際にstate
の状態によって昇順か降順を切り替える。そしてボタンを1つ設置し、onclick
でstate
を切り替えるというようにしていきます。
まずはボタンを設置します。
遷移リストをレンダリングするGameコンポーネント
をいじっていきます。
class Game extends React.Component {
︙
︙
render() {
︙
︙
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>
<div><button>ASC⇔DESC</button></div> //追加
<ol>{moves}</ol>
</div>
</div>
);
}
}
これによってまずボタンが設置されました。
次に昇順か降順を保持するstate
を増やします。
チュートリアル本編でも出てきたように、state
はなるべく親階層で持つべきでした。
なので、Gameコンポーネント
にisAsc
というstate
を追加します。
isAsc
がtrue
の場合は昇順、false
の場合は降順とします。
class Game extends React.Component {
constructor() {
super();
this.state = {
history: [{
squares: Array(9).fill(null)
}],
xIsNext: true,
stepNumber: 0,
isAsc: true //追加
};
}
︙
}
遷移リストをレンダリングする際に、次に追加したisAscをもとに昇順か降順かどうかを考慮するように書き換えます。
class Game extends React.Component {
︙
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>
<div><button>ASC⇔DESC</button></div>
<ol>{this.state.asc ? moves : moves.reverse()}</ol> //変更
</div>
</div>
);
}
}
最後に、isAsc
を切り替えるメソッドを定義して、ボタンをクリックした際に呼び出すようにします。
class Game extends React.Component {
︙
//追加
toggleAsc() {
this.setState({
asc: !this.state.asc,
});
}
︙
render() {
︙
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>
<div><button onClick={() => this.toggleAsc()}>ASC⇔DESC</button></div> //変更
<ol>{this.state.asc ? moves : moves.reverse()}</ol>
</div>
</div>
);
}
}
変更は以上です。
実際にちゃんと動くかどうかを確認していきます。
↓このようにボタンで切り替えれるようになっています。
以上で課題4が完成です!
課題5. 誰かが勝った時に、3つ揃ったセルをハイライトする
方針として、勝者がいるかどうかをcalculateWinner関数
で判定していますが、こちらが勝者がいない場合にnull
を、いる場合に勝者のプレイヤー名の文字列を返していましたが、同時にどのマスの列が揃ったかの配列も返すようにします。
それをもとに、マスを描画する際に、揃っているマスなのかどうかを判断し、そうであればハイライトする用のclass
を付与していきます。
ということで、まずはcalculateWinner関数
を変更していきます。
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 {
winner: squares[a], //変更
line: [a, b, c] //変更
};
}
}
return null;
}
このように列が揃った場合の返り値に揃った列の情報も返すようにしました。
次にGameコンポーネント
を書き換えます。
calculateWinner
の返り値を変更したのでそれに伴い修正しています。
加えてBoardコンポーネント
に揃った列のマスを渡すようにしています。
class Game extends React.Component {
︙
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const settlement = calculateWinner(current.squares); //変更
let status;
if (settlement) {
status = 'Winner: ' + settlement.winner; //変更
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
const moves = history.map((step, move) => {
const desc = move ?
'Move #' + move + '(' + step.col + ',' + step.row + ')':
'Game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)} className={this.state.stepNumber === move ? 'bold' : '' } >{desc}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
highlightCells={settlement ? settlement.line : []} //追加
/>
</div>
<div className="game-info">
<div>{status}</div>
<div><button onClick={() => this.toggleAsc()}>ASC⇔DESC</button></div>
<ol>{this.state.asc ? moves : moves.reverse()}</ol>
</div>
</div>
);
}
}
Boardコンポーネント
も書き換えます。
揃った列のマスの配列を受け取って、マスをレンダリングする際に、そのマスが揃ったマスに含まれているかどうかを判定し、isHighlight
で渡すようにしています。
class Board extends React.Component {
renderSquare(i, isHighlight = false) { //変更
return <Square isHighlight={isHighlight} key={i} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />; //変更
}
render() {
return (
<div>
{
Array(3).fill(0).map((row, i) => {
return (
<div className="board-row" key={i}>
{
Array(3).fill(0).map((col, j) => {
return(
this.renderSquare(i * 3 + j, this.props.highlightCells.indexOf(i * 3 + j) !== -1) //変更
)
})
}
</div>
)
})
}
</div>
);
}
}
次にSquareコンポーネント
を変更します。
isHighlight
がtrue
のときのみhighlight
クラスを付与するようにしています。
function Square(props) {
return (
<button className={`square ${props.isHighlight ? 'highlight' : ''}`} onClick={() => props.onClick()}>
{props.value}
</button>
);
}
仕上げにhighlightクラス
がついたときに背景色が変わるようにcss
を編集します。
.highlight {
background-color: yellow;
}
以上で修正は完了です。
実際に挙動を確認していきます。
↓このように決着がついたときにハイライトされるようになりました。
これで課題5は完了です!
課題6. 引き分けの時にはdrawを表示
方針としては、課題5と同様にcalculateWinner関数
で引き分けかどうかの情報も返すようにします。
まず、calculateWinner関数
を以下のように変更していきます。
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 {
isDraw: false, //追加
winner: squares[a],
line: [a, b, c]
};
}
}
//ここから追加
if (squares.filter((e) => !e).length === 0) {
return {
isDraw: true,
winner: null,
line: []
}
}
//ここまで追加
return null;
}
次にGameコンポーネント
のrenderメソッド
を書き換えていきます。
class Game extends React.Component {
︙
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const settlement = calculateWinner(current.squares);
let status;
if (settlement) {
//ここから追加
if (settlement.isDraw) {
status = 'Draw';
} else {
status = 'Winner: ' + settlement.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)}
highlightCells={settlement ? settlement.line : []}
/>
</div>
<div className="game-info">
<div>{status}</div>
<div><button onClick={() => this.toggleAsc()}>ASC⇔DESC</button></div>
<ol>{this.state.asc ? moves : moves.reverse()}</ol>
</div>
</div>
);
}
}
変更は以上です。
実際の動作を確認していきましょう。
↓引き分けの際に「Draw」が表示されることが確認できました!
以上で追加課題のすべてが完了です。
まとめ
今回は、公式のreactチュートリアルの追加課題を行っていきました。
難易度としてはそこまで難しくなくて、チュートリアルの本編をちゃんと理解していればできる内容であったかなと思います。
今回のコードは本編も含めてGithubのリポジトリにまとめているので、よかったら参考にしてください。
https://github.com/fkt1993/react-sample-app
この記事が学習の参考になっていただければうれしいです。
(2024/12/11 15:33:50時点 Amazon調べ-詳細)