はじめに
Reactチュートリアルの追加課題の解答と、私が解いていく中で詰まったところを記述する。ReactやJSに関する知識が拙い中解いたので、より良い解き方などがあると考えられるが、一例として参考にしていただきたい。(より良い解き方など指摘いただければ追記致します。)
コードはチュートリアル内の最終結果を改変した。
1.履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。
考え方
着手の位置を履歴として残すために、history
に新たな要素として追加する。その後moves
に着手の位置も表示するように変更すればよい。
解答例
まず、Game
クラスのコンストラクタにあるhistory
と盤面が動いた際の処理が書かれたメソッドhandleClick
内のhistory
にputState
をkeyとした着手の位置を保存するための要素を追加する(history自体ではなくhistory内にある辞書やhistoryに追加する辞書に対しての意)。
コンストラクタでは以下のように変更する(ここで定義しなくても動く)。
history: [
{
squares: Array(9).fill(null),
putState: "(0, 0)"
}
]
だが、handleClick
メソッドでは着手の位置を与える必要がある。幸運なことに(当然だが)このメソッドは選択したマスの番号を引数として与えられているので、以下のように変更するだけでよい。
history: history.concat([
{
squares: squares,
putState: ` (${i/3|0}, ${i%3})`
}
])
JavaScriptのバージョンによっては文字列の埋め込みがこのようにできないので以下のようにする。
history: history.concat([
{
squares: squares,
putState: " (" + (i/3|0).toString() + ", " + (i%3).toString() + ")"
}
])
このようにして着手の位置を履歴として残せるようになったので最後にmoves
を変更し、着手の位置を表示させる。
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move + step["putState"]:
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
2.着手履歴のリスト中で現在選択されているアイテムをボールドにする。
考え方
選択されている盤面はGame
クラスのメンバ変数StepNumber
に格納されている。また、着手履歴の表示に関する部分はmoves
に格納されている。このmoves
は着手履歴を保存しているhistory
を用いてrenderされる度に生成される。さらに表示したい盤面を着手履歴のリストから選択するたびStepNumber
が更新され再度renderされる。つまり、選択のたびrenderされるのでmovesが生成されるときにStepNumber
と一致する番目にあるアイテムをボールドするようにすれば良いと考えられる。
解答例
変更点はmoves
の定義部分のみ
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)}>
{
move === this.state.stepNumber
? <b>{desc}</b>
: desc
}
</button>
</li>
);
});
最初falseの時のdesc
を{desc}
と書いていてerror({}が二重になるため)を出していたのでこの部分は注意したい。
3.Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。
考え方
単純にリストを作って二重ループ回してリストにJSXを適宜格納し、ハードコーディング部分をリストに置き換える。
解答例
考え方通りコードを書く。
render() {
const squareBoard = [];
const row = 3;
const col = 3;
for(let i=0; i<row; i++){
let rowBoard = [];
for(let j=0;j<col; j++){
rowBoard.push(this.renderSquare(j+3*i));
}
squareBoard.push(
<div className="board-row">
{rowBoard}
</div>
)
}
return (
<div>
{squareBoard}
</div>
);
}
これで上手くいくと考えていたが警告が出る。
```これはkeyに関する警告でチュートリアル中にも触れられていたものである。keyを設定することで[VirtualDOM](https://stackoverflow.com/questions/21965738/what-is-virtual-dom)のdiffから実際のDOMに反映させるときの変更を最小限にすることができる。変更を施すと以下のようになる。
```Javascript
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
key={"square-"+i.toString()}
/>
);
}
render() {
const squareBoard = [];
const row = 3;
const col = 3;
for(let i=0; i<row; i++){
let rowBoard = [];
for(let j=0;j<col; j++){
rowBoard.push(this.renderSquare(j+3*i));
}
squareBoard.push(
<div className="board-row" key={"row-"+i.toString()}>
{rowBoard}
</div>
)
}
return (
<div>
{squareBoard}
</div>
);
}
}
ここでは"row-"+i.toString()
としているがiだけでも問題ない(Square
も同様)。
4.着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。
私はトグルボタンそのものを実装することは本質ではなく、コードが複雑になると考えたので単純にボタンで実装した(トグルボタンにしたい方はトグルボタンのコンポーネントを作成してボタンのタグをトグルボタンのタグに変更することでできる)。
考え方
着手履歴は、Game
クラスのメンバ変数history
に格納されており、moves
によって実装されている。moves
はhistory
から履歴を一手ずつ取ってきてJSXにしたものをリストにしている。history
は常に昇順であるためこれから作られたmoves
もまた常に昇順となっている。つまり降順で表示したいときはmoves
を逆順にすると良いと考えられる。
解答例
昇順、降順ボタンを任意の位置(ここでは着手履歴の上)に設定し、それを押すことで新たに追加するGame
のメンバ変数ascending
の値を変える。ascending
は昇順でtrue、降順でfalseとなるようにする。ascending
の値によってrenderの直前にmoves
を逆順にするかどうかを決める。
constructor(props) {
super(props);
this.state = {
history: [
{
squares: Array(9).fill(null)
}
],
stepNumber: 0,
xIsNext: true,
ascending: true,
};
}
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");
}
if(!this.state.ascending) moves.reverse();
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>
<button onClick={()=>this.setState({ascending: true})}>昇順</button>
<button onClick={()=>this.setState({ascending: false})}>降順</button>
<ol>{moves}</ol>
</div>
</div>
);
}
一つ目はGame
クラスのコンストラクタ部分で、二つ目はGame
クラスのrender部分である。トグルボタンでは押したときにascending
をtrueやfalseを切り替えるように実装してやれば良い。
<ToggleButton onClick={()=>this.setState({ascending: !ascending})}/>
5.どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。
考え方
勝敗が決定したときにハイライトさせるので勝敗を確認するcalculateWinner
関数と勝敗によって値が変わるGame
クラス内のstatus
を定義している部分をうまく変更すれば良いと考えられる。まず勝敗につながった3マスをどのように抜き出すかを考える。calculateWinner
関数では勝利につながる可能性のある3マスの組を全列挙しそのうちの一つでも3マス全てが同じ記号であればその記号を返し、1つもなければnullを返している。つまり、calculateWinner
関数では勝敗を決まった時勝利につながった3マスを特定することができ、返り値にその3マスも返すようにすることが可能であると考えられる。次に抜き出した情報をもとにハイライトを行う方法を考える。勝敗が決まった時のみこの処理を行うのでハイライトを行う機能はstatus
を定義する場面で行う。勝敗が決まった時の処理でcurrent.squares
の先ほど抜き出した3マスをハイライトする機能を追加すると良いと考えられる。
解答例
まず、clulculateWinner
関数の返り値を既存のものと勝利につながった3マスを合わせたものへと変更する。
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], lines[i]];
}
}
return [null, []];
}
返り値を変えたことによって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)[0] || squares[i]) {//ここだよ
return;
}
squares[i] = this.state.xIsNext ? "X" : "O";
this.setState({
history: history.concat([
{
squares: squares
}
]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
}
最後にstatus
内の処理に一手間加える。
let status;
if (winner[0]) {
status = "Winner: " + winner[0];
for(let i in winner[1]) current.squares[winner[1][i]] = <font color="#F00">{current.squares[winner[1][i]]}</font>;
} else {
status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}
ハイライトは3つのマスを赤色にすることにした(これは何でもいい)。
しかし、この方法だと勝利につながったマスが5つの時であっても3つしかハイライトされない。このようなケースは通常人間同士が真剣に行ったときに出ることはないが、気持ち悪いので5つともハイライトされるようにする。変更したのは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]
];
let winner = null;
let winLines = [];
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]) {
winLines = winLines.concat(lines[i]);
winner = squares[a];
}
}
return [winner, winLines];
}
6.どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。
考え方
引き分けのメッセージはstatus
に新たな分岐を作ることによって、引き分けの判別はcalculateWinner
関数で引き換えの処理を加えればよいと考えられる。また、引き分けは勝敗が決まってない時のsquares
の中身にnullがあるかないかで判別することができる。
###解答例
まず、status
はcalculateWinner
関数の返り値に依存しているので、calculateWinner
関数を変更する。この関数はhandleClick
メソッドにも使われており、それに影響が出ないような形で変更したい。つまり、bool値がtrueとなるような値になるようにする。
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];
}
}
if (squares.indexOf(null) === -1) return "draw";
return null;
}
最後にstatus
の定義部分を変更する。引き分けになった時の表示はDrawにした。
let status;
if (winner === "draw") {
status = "Draw";
} else if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}
おわりに(全体コード)
追加課題を全て終えた後はこのようなコードになった。より良い書き方や書き方の問題などがあれば追記するので教えていただきたい。