過去にprogateで基礎に触れてから長らく放置したままだったReactの勉強を本格的に始めることにしました。
というのも、遠くない未来で受ける案件において、Reactを使わないことは避けられそうにないからです。
その案件自体はVue.jsでも出来なくは無さそうですが、「せっかくなので一通り使えるようになりたい(むしろフロントエンドやるなら今時使えないとヤバい)」思っているのと、「Reactが分かるようになればWordPressのブロック開発も出来るようになる」という理由もあるからです。
とりあえず公式ドキュメントを読み始めて、チュートリアルの三目並べゲームを作りましたが、これが中々分かりやすくて面白く、回答が書かれていない改良問題にも早速挑戦してみましたので、その解答例を記載しておきます。
- 掲載元(React公式ドキュメント)
- 問題
- 動作テスト用サンプル
掲載元(React公式ドキュメント)
問題
「1. 履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。」
3×3のマスのうち、左上をcol:1, row:1、右下をcol:3, row:3とします。
index.js
修正
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null)
}],
stepNumber: 0,
xIsNext: true,
clickPosition: { //追加
col: null,
row: null
}
};
}
/*略*/
}
修正
class Game extends React.Component {
/*略*/
handleClick(i) {
/*略*/
const position = getPosition(i); // 追加
/*略*/
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
clickPosition: position // 追加
});
}
/*略*/
}
修正
class Game extends React.Component {
/*略*/
render() {
/*略*/
return (
<div className="game">
/*略*/
<div>col:{this.state.clickPosition.col}, row:{this.state.clickPosition.row}</div>
/*略*/
</div>
);
}
}
追加
function getPosition(index) {
let pos = [];
for (let row = 1; row <= 3; row++) {
for (let col = 1; col <= 3; col++) {
pos.push({
row: row,
col: col
});
}
}
return pos[index];
}
新規定義したgetPosition関数は、下記のような配列を作って、クリックされた位置(0~8)と一致するインデックスの要素の値を返します。
{row:1, col:1},{row:1, col:2},{row:1, col:3},
{row:2, col:1},{row:2, col:2},{row:2, col:3},
{row:3, col:1},{row:3, col:2},{row:3, col:3},
「2. 着手履歴のリスト中で現在選択されているアイテムを太字にする。」
index.js
修正
class Game extends React.Component {
/*略*/
render() {
/*略*/
const moves = history.map((step, move) => { // step: value of array(history) , move: index of array(history)
/*略*/
const className = move === this.state.stepNumber ? 'active' : '' ; // 追加
return ( // className追加
<li key={move} className={className}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
/*略*/
}
/*略*/
}
move(0~8)がstateのstepNumberと一致するときはliタグにクラス名’active’を出力します。
index.css
追加
li.active button {
font-weight: bold;
}
「3. Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。」
index.js
修正
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
key={i}/*追加*/
/>
);
}
/*略*/
}
追加
“2 つのループ”を使う場合の処理内容に自信が無いですが、一応動きます。
class Board extends React.Component {
/*略*/
createBoardHtml() { // 追加
let square, div = [];
let num = 0;
for (let i = 0; i < 3; i++) {
square = [];
for (let j = 0; j < 3; j++) {
num = 3 * i + j;
square.push(this.renderSquare(num));
}
div.push(<div className="board-row" key={i}>{square}</div>);
}
return div;
}
/*略*/
}
renderSquare関数のSquareおよびcreateBoardHtml関数のdivにkey={i}を記述しないと下記警告を受けます。
Warning: Each child in a list should have a unique “key” prop.
リスト(配列)を出力する際は、それぞれにユニークなキーを付与する必要があります。
修正
class Board extends React.Component {
/*略*/
render() {
return (
<div>
{this.createBoardHtml()/*修正*/}
</div>
);
}
}
「4. 着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。」
参考
index.css
追加
.toggle {
display: flex;
}
.toggle input {
display: none;
}
.toggle label{
/*display: block;
float: left;*/
cursor: pointer;
width: auto;
margin: 0;
padding: 0.25rem;
background: #bdc3c7;
color: #869198;
font-size: 0.75rem;
text-align: center;
line-height: 1;
transition: .2s;
}
.toggle label:first-of-type{
border-radius: 3px 0 0 3px;
}
.toggle label:last-of-type{
border-radius: 0 3px 3px 0;
}
.toggle input[type="radio"]:checked + .switch-on {
background-color: #a1b91d;
color: #fff;
}
.toggle input[type="radio"]:checked + .switch-off {
background-color: #e67168;
color: #fff;
}
index.js
追加
class Toggle extends React.Component {
render() {
return (
<div className="toggle">
<span>History order: </span>
<input type="radio" name="order" id="asc" value="asc" checked={this.props.order === 'asc'}
onChange={() => this.props.onChange()}/>
<label htmlFor="asc" className="switch-on">ASC</label>
<input type="radio" name="order" id="desc" value="desc" checked={this.props.order === 'desc'}
onChange={() => this.props.onChange()}/>
<label htmlFor="desc" className="switch-off">DESC</label>
</div>
)
}
}
トグルボタンを表示するコンポーネントです。inputとlabelについては、属性の値のみ定義したオブジェクトの配列を用意してarray.map()で出力した方がスマートかもしれません。
修正
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
/*略*/
order: 'asc',
};
}
}
昇順の設定状態を管理するstateを追加します。
追加
class Game extends React.Component {
/*略*/
handleChange() {
const order = this.state.order === 'asc' ? 'desc' : 'asc';
this.setState({
order: order
});
}
/*略*/
}
トグルボタンをクリックされた時の処理を定義します。(子コンポーネントToggleを通して呼び出される)
選択中のボタン(実態はradio)をクリックしても、onChangeイベントは発生しない(checkedされているradioは変わらない)ので、選択されている状態は取得せず単純に今のstateの状態(asc or desc)の逆をセットするだけで良いです。
この関数によってstateの変数が更新されると再レンダリングが発生するので、ここでヒストリーボタンの昇順並び替え処理を行う必要はありません。
修正
class Game extends React.Component {
/*略*/
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
let moves = history.map((step, move) => { // 修正:const -> let
const desc = move ?
'Go to move #' + move :
'Go to game start';
const className = move === this.state.stepNumber ? 'active' : '' ;
return (
<li key={move} className={className}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
if (this.state.order === 'desc') { // 追加
moves = moves.reverse();
}
/*略*/
return (
<div className="game">
/*略*/
<Toggle
order={this.state.order}
onChange={() => this.handleChange()}
/>
/*略*/
</div>
);
}
}
変数movesは再代入を行う為にconstからletへ変更し、直後に昇順がdescなら逆順に並び替えます。
render()ではトグルボタンを表示するToggleコンポーネントを呼び出します。その際、昇順設定とonChange関数をpropsで渡します。
「5. どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。」
index.css
追加
.win {
background-color: cyan;
}
index.js
修正
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': lines[i] // winner's line
};
}
}
return { // 修正
'winner': null,
'line': null
};
}
勝者(O or X)に関する情報だけでなく、勝敗決定の要因となった勝者選択マスの座標情報lines[i](数値0~8を値として持つ要素数3の配列)も返すように、返り値の構造を変更します。
修正
class Game extends React.Component {
/*略*/
handleClick(i) {
/*略*/
if (calculateWinner(squares).winner || squares[i]) { // 修正
return;
}
/*略*/
}
/*略*/
}
勝敗をチェックするcalculateWinner関数の返り値の構造を変更したので、それに合わせて修正します。
修正
class Game extends React.Component {
/*略*/
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const win = calculateWinner(current.squares); // 修正
const winner = win.winner; // 追加
const line = win.line; // 追加
/*略*/
return ( // 修正
<div className="game">
/*略*/
<Board
line={line}
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
/*略*/
</div>
);
}
/*略*/
}
勝敗をチェックするcalculateWinner関数の返り値の構造を変更したので、それに合わせて修正します。
また、勝者選択マスの座標情報(null or配列)を”line”として子のBoardコンポーネントへ渡します。
修正
class Board extends React.Component {
renderSquare(i) {
let className = 'square';
if (
this.props.line !== null
&& this.props.line.indexOf(i) !== -1
) {
className += ' win';
}
return (
<Square
className={className}
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
key={i}
/>
);
}
/*略*/
}
子のSquareコンポーネントに渡すcssのクラス名として”square”をデフォルトでセットし、親コンポーネントから受け取った勝者選択マスの座標情報の”line”(null or配列)がnullで無い時(勝利が確定している場合)に、描画しようとするマス番号i(0~8)が含まれていたら、cssのクラス名に” win”を追加します。
修正
function Square(props) {
return (
<button className={props.className} onClick={props.onClick}>
{props.value}
</button>
);
}
class名を直書きでは無く、親コンポーネントから受け取るように変更します。
「6. どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。」
index.js
修正
class Game extends React.Component {
/*略*/
render() {
/*略*/
let status ;
if (winner) {
status = 'Winner: ' + winner;
} else {
if (this.state.stepNumber == 9) // 追加
status = 'Result:draw';
else
status = 'Next player:' + (this.state.xIsNext ? 'X' : 'O');
}
/*略*/
}
/*略*/
}
利者判定の偽(else)の処理において、ステップ数(stepNumber)が9の場合は”Next player:”の代わりに”Result:draw”を表示します。
動作テスト用サンプル
See the Pen Answer example of React official tutorial by YUKI (@yuki84web) on CodePen.