追加課題の解答例
Reactの公式チュートリアルの追加課題の解答とその解説です。
React初心者なので、間違いがあったら指摘してもらえるとありがたいです。
コードはモジュール化し、
- App.js -> Game, calclateWinnerコンポーネント
- Board.js -> Boardコンポーネント
- Square.js -> Squareコンポーネント
となっています。コード全体は、GitHub上にあります。
目次
追加課題1
現在の着手の部分だけ、ボタンではなく “You are at move #…” というメッセージを表示するようにする。
解説1
現在の着手の状態はcurrentMoveにあるので、currentMoveの値によって場合分けをする。
// App.js
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li key={move}>
{/* 三項演算子 */}
{move === currentMove ? (
<div className="bold">You are at move#...</div>
) : (
<button onClick={() => jumpTo(move)}>{description}</button>
)}
</li>
);
});
このように、三項演算子を使用して move === currentMove の場合には "You are at move#..." を表示し、それ以外の場合にはボタンを表示しています。
追加課題2
マス目を全部ハードコードするのではなく、Board を 2 つのループを使ってレンダーするよう書き直す。
解説2
まず、一部分だけfor文で書き直してみる。keyが必要なことに注意する。
// Boar.js
const maxCol = 3;
const rowSquare1 = [];
for (let col = 0; col < maxCol; col++) {
const index = maxCol * 0 + col;
rowSquare1.push(
<Square
value={squares[index]}
onSquareClick={() => handleClick(index)}
key={index} //keyを追加
/>
);
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
{rowSquare1}
</div>
<div className="board-row">
<Square value= {squares[3]} onSquareClick={() => handleClick(3)} />
<Square value= {squares[4]} onSquareClick={() => handleClick(4)} />
<Square value= {squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value= {squares[6]} onSquareClick={() => handleClick(6)} />
<Square value= {squares[7]} onSquareClick={() => handleClick(7)} />
<Square value= {squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
同様に、他の部分も書き直す。
// Board.js
const maxCol = 3;
const rowSquare1 = [];
for (let col = 0; col < maxCol; col++) {
const index = maxCol * 0 + col;
rowSquare1.push(
<Square
value={squares[index]}
onSquareClick={() => handleClick(index)}
key={index}
/>
);
}
const rowSquare2 = [];
for (let col = 0; col < maxCol; col++) {
const index = maxCol * 1 + col;
rowSquare2.push(
<Square
value={squares[index]}
onSquareClick={() => handleClick(index)}
key={index}
/>
);
}
const rowSquare3 = [];
for (let col = 0; col < maxCol; col++) {
const index = maxCol * 2 + col;
rowSquare3.push(
<Square
value={squares[index]}
onSquareClick={() => handleClick(index)}
key={index}
/>
);
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
{rowSquare1}
</div>
<div className="board-row">
{rowSquare2}
</div>
<div className="board-row">
{rowSquare3}
</div>
</>
);
二重ループで書き直す。
// Board.js
const boardSquare = [];
const maxRow = 3;
const maxCol = 3;
for (let row = 0; row < maxRow; row++) {
const rowSquare = [];
for (let col = 0; col < maxCol; col++) {
const index = maxCol * row + col;
rowSquare.push(
<Square
value={squares[index]}
onSquareClick={() => handleClick(index)}
key={index}
/>
);
}
boardSquare.push(
<div className="board-row" key={'row-' + row}> //keyが必要
{rowSquare}
</div>
);
}
return (
<>
<div className="status">{status}</div>
{boardSquare}
</>
);
最後に関数として切り出す。
// Board.js
const generateBoard = () => {
const maxRow = 3;
const maxCol = 3;
const board = [];
for (let row = 0; row < maxRow; row++) {
const rowSquare = [];
for (let col = 0; col < maxCol; col++) {
const index = maxCol * row + col;
rowSquare.push(
<Square
value={squares[index]}
onSquareClick={() => handleClick(index)}
key={index}
/>
);
}
board.push(
<div className="board-row" key={`row-${row}`}>
{rowSquare}
</div>
);
}
return board;
}
return (
<>
<div className="status">{status}</div>
{generateBoard()}
</>
);
完成!
追加課題3
手順を昇順または降順でソートできるトグルボタンを追加する。
解説3
現在は昇順に並んでいるので、まず、降順で並べ替えてみる。
手順の状態はmovesが持っているので、reverseメソッドを使って、
// App.js
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li key={move}>
{move === currentMove ? (
<div>You are at move#...</div>
) : (
<button onClick={() => jumpTo(move)}>{description}</button>
)}
</li>
);
});
moves.reverse(); // 追加!
次に、順番の状態を新たなstateで管理する。
// App.js
const Game = () => {
const [history, setHistory] = useState([Array(9).fill('')]);
const [currentMove, setCurrentMove] = useState(0);
const [movesOrder, setMovesOrder] = useState(false); // 追加!
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
// stateの状態で場合分けをする。
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li key={move}>
{move === currentMove ? (
<div>You are at move#...</div>
) : (
<button onClick={() => jumpTo(move)}>{description}</button>
)}
</li>
);
});
if (movesOrder) {
moves.reverse();
}
// ボタンとイベントを追加する。
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<button className="toggle-button" onClick={() => {setMovesOrder(!movesOrder)}}> // 追加!
On/Off
</button>
<ol>{moves}</ol>
</div>
</div>
);
次にindex.cssを編集。
// index.css
// 以下を追加
.toggle-button {
background-color: #3498db;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
outline: none;
font-size: 16px;
transition: background-color 0.3s ease;
}
.toggle-button:hover {
background-color: #2980b9;
}
追加課題4
4 - 1 どちらかが勝利したときに、勝利につながった 3 つのマス目をハイライト表示する。
4 - 2 引き分けになった場合は、引き分けになったという結果をメッセージに表示する。
解説4
解説4 - 1
現在、勝利者の判定はcalclateWinner関数で行われているので、勝利者を返す際に勝利につながったマス目の情報も返すようにする。
// App.js
export function calclateWinner(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]
];
// 勝者とマスの情報をもつオブジェクトを用意
const result = {
winner: '',
winLine: [],
}
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]) {
result.winner = squares[a]; // 勝者
result.winLine = [...lines[i]]; // マスの情報
}
}
return result; // resultを返す
}
calclateWinnerを呼び出していたところを変える。
// Board.js
function handleClick(i) {
if (squares[i] || calclateWinner(squares).winner) { // 変更!
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
const winner = calclateWinner(squares).winner; // 変更!
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
マス目をレンダリングする際に、そのマス目が勝利につながったマス目の情報に含まれているかで場合分けをし、含まれていたらハイライト表示をするようにする。
まずBoard.jsに勝利につながったマス目の情報を渡し、含まれているかどうか確認する。
// App.js
const Game = () => {
const [history, setHistory] = useState([Array(9).fill('')]);
const [currentMove, setCurrentMove] = useState(0);
const [movesOrder, setMovesOrder] = useState(false);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
const { winner, winLine } = calclateWinner(currentSquares); // 追加!
// 途中省略
return (
<div className="game">
<div className="game-board">
<Board
xIsNext={xIsNext}
squares={currentSquares}
onPlay={handlePlay}
winLine={winLine} // 追加!
/>
</div>
// Board.js
const Board = ({ xIsNext, squares, onPlay, winLine }) => { // 追加!
const generateBoard = () => {
const maxRow = 3;
const maxCol = 3;
const board = [];
for (let row = 0; row < maxRow; row++) {
const rowSquare = [];
for (let col = 0; col < maxCol; col++) {
const index = maxCol * row + col;
const isHighlight = winLine.includes(index); // 追加!
rowSquare.push(
<Square
value={squares[index]}
onSquareClick={() => handleClick(index)}
key={index}
isHighlight={isHighlight}
/>
);
}
board.push(
<div className="board-row" key={`row-${row}`}>
{rowSquare}
</div>
);
}
return board;
}
// 以下略
次に、Square.jsで場合分けし、ハイライト表示する。
// Square.js
const Square = ({ value, onSquareClick, isHighlight }) => { // 追加!
const className = isHighlight ? "square-highlight" : "square"; // 追加!
return (
<button className={className} onClick={onSquareClick}> // 変更
{value}
</button>
);
}
最後にcssを追加して完成。
// index.css
.square-highlight {
background-color: #fade5e;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
解説4 - 2
calculateWinner関数で引き分けかどうかの情報も返すようにすればよい。
なので、calclateWinner関数を変更し、
// App.js
export function calclateWinner(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]
];
const result = {
winner: '',
winLine: [],
isDraw: false, // 追加!
}
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]) {
result.winner = squares[a];
result.winLine = [...lines[i]];
}
}
// 勝者が存在せず、マス目がすべて埋まっている場合、引き分け
if (result.winner === '' && !squares.includes('')) {
result.isDraw = true;
}
return result;
}
次に、引き分けの場合メッセージを表示する。
winner変数はBoard.jsにあるので、そこに引き分けの情報を渡し、表示する。
やり方は4 - 1と同じ方法。
// App.js
const Game = () => {
const [history, setHistory] = useState([Array(9).fill('')]);
const [currentMove, setCurrentMove] = useState(0);
const [movesOrder, setMovesOrder] = useState(false);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
const { winner, winLine, isDraw } = calclateWinner(currentSquares); // 変更!
// 途中省略
return (
<div className="game">
<div className="game-board">
<Board
xIsNext={xIsNext}
squares={currentSquares}
onPlay={handlePlay}
winLine={winLine}
isDraw={isDraw} // 追加!
/>
</div>
<Board.js>
const Board = ({ xIsNext, squares, onPlay, winLine, isDraw }) => { // 変更!
// 途中省略
const winner = calclateWinner(squares).winner;
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
// 引き分けの場合、statusにDrawを入れる
if (isDraw) {
status = 'Draw';
}
追加課題5
着手履歴リストで、各着手の場所を (row, col) という形式で表示する。
解説5
ぱっと思いつくのは、マス目を作成するときに使った、row, colを利用することですが、情報がBoard.jsにあり、その情報を使いたいのは,App.jsなので、難しそうです。リフトアップするにしても大改造になりそうなので、別の方法を考えます。
今着手の位置はindexで持っているので、そこから座標を出すことができそうです。着手の履歴はhistoryをもとにしているので、historyに着手の位置を持たせれば良さそうです。
まず、App.jsについて
historyを変更し、それに伴ってcurrentSquaresとhandlePlayを変更します。
// App.js
const Game = () => {
const [history, setHistory] = useState([
{
squares: Array(9).fill(''),
position: null
}
]);
const [currentMove, setCurrentMove] = useState(0);
const [movesOrder, setMovesOrder] = useState(false);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove].squares;
const { winner, winLine, isDraw } = calclateWinner(currentSquares);
function handlePlay(nextSquares, position) {
const nextHistory = [
...history.slice(0, currentMove + 1),
{ squares: nextSquares, position: position },
];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
// movesを変更し、着手を表示する。
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = `Go to move # ${squares.position}`;
} else {
description = 'Go to game start';
}
次に、Boad.jsについて
handleClickでindexをもとにrow, colを生成しonplayに渡す。
// Board.js
const Board = ({ xIsNext, squares, onPlay, winLine, isDraw }) => {
function handleClick(i) {
if (squares[i] || calclateWinner(squares).winner) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
const row = Math.floor(i / 3) + 1;
const col = i % 3 + 1;
const position = [row, col]
onPlay(nextSquares, position);
}
history状態変数で過去の着手をオブジェクトとして保持することが難しかったです。
最後に
もっときれいな実装があるように思えますが、とりあえずの完成としておきます。
追加課題を通して、Reactにちょっと慣れることができたので、個人的に課題やってよかったです。皆さんもぜひ。