Reactのチュートリアルを全部やってみたので、コードを共有します。
いわゆる「〇×ゲーム」を作るというやつです。
というのも
最初のほうはすべてコードの指示があるのですが、文書が進むにしたがってコードの記載が減っていきます。最後は宿題(?)のような5個の追加実装をやってみようみないな感じになります。そのあたりで躓いている方がもしいたら参考になるかと思いましたので共有します。もちろん、自分で考えて実装をしたほうがためになりますし、間違えがあるかもしれないので、参照する場合にはそのあたりをご了承ください。
あとは、自分の備忘目的というのもあります。
チュートリアルをするにあたって
チュートリアルにも記載があるのですが、JavaScriptの基本的な知識とES2015を少し知っている必要があります。
チュートリアルへのリンク
日本語訳など
Reactの公式ドキュメントが2016年10月に更新されたようで、現在公式の日本語訳がありません。日本語訳とそのあたりの情報は現在こちらで確認できます。
http://mae.chab.in/archives/2943
コードへのリンク
Reactのチュートリアルで紹介されていたCODEPENのReactのチュートリアル用のコードをforkして作成しています。
https://codepen.io/tyahha/pen/RKbrwO
※Apple Penを思い出したのは僕だけではないはずです
動きだけ見たい場合はこちら
Wrapping Upの対応概要
訳は上述のこちらのサイトより引用させていただいております。
1. Display the move locations in the format "(1, 3)" instead of "6".
訳:動作の場所を「6」ではなく「(1, 3)」というフォーマットで表示させてみましょう。
- Gameコンポーネントのstateのhistoryにselectを追加して、そのときどのマスをクリックしたかを保持
this.state = {
history: [
{
squares: Array(9).fill(null),
select: null,
},
],
...
- Game#handleClickに保存処理を追加
...
this.setState({
history: history.concat([{squares, select: i}]),
...
- 選択値(0~8)を「(X, Y)」という文字列に変換する関数を用意。
これはいらないかもしれません。なんとなくあったほうがわかりやすかと思って。
selectToDisplay(select) {
return `(${Math.floor((select)/3) + 1}, ${select%3 + 1})`;
}
- Game#renderの「desc」を変更
ついでに、そのターンで'X'と'O'のどっちを置いたかわかるようにしておきました。
...
const moves = history.map((step, move) => {
const desc = move ? `Move #${move} ${this.selectToDisplay(step.select)}:${move % 2 ? 'X' : 'O'}` : 'Game start';
...
2. Bold the currently-selected item in the move list.
訳:動作リストで現在選択されているアイテムを太字にしてみましょう。
- 上記で追加したselectを利用
- Game#render内でmovesを作成するmap内にて現在選択しているhistory(current)のselectと一致するhistoryがあった場合はリンクをで囲むように修正。
...
const moves = history.map((step, move) => {
const desc = move ? `Move #${move} ${this.selectToDisplay(step.select)}:${move % 2 ? 'X' : 'O'}` : 'Game start';
let anchor = <a
href="#"
onClick={() => this.jumpTo(move)}
>{desc}</a>
let link;
if (current.select === step.select) {
link = <b>{anchor}</b>;
}
else {
link = anchor;
}
return (
<li key={move}>
{link}
</li>
);
});
...
3. Rewrite Board to use two loops to make the squares instead of hardcoding them.
訳:マスをハードコーディングする代わりに2つのループを使って書き直してみましょう。
- コードを見てもらうしかない。
- 変数を用意しないでJSX内でループを記述するやり方があるのだろうか?
render() {
const rows = [];
for (let y = 0; y < 3; y++) {
const squares = [];
for (let x = 0; x < 3; x++) {
squares[x] = this.renderSquare(y * 3 + x);
}
rows.push(
<div key={y} className="board-row">
{squares}
</div>
);
}
return (
<div>{rows}</div>
);
4. Add a toggle button that lets you sort the moves in either ascending or descending order.
訳:動作リストを昇順、降順で並び替えるトグルボタンを追加してみましょう。
- Gameのstateの現在のソート状態を保持するstate「isDesc」を追加
- なぜisAscにしなかったのかはわかりません。
- トグルボタンを追加するときに右側の表示が多くなってきたので「Next player: O」とかでるヘッダのようなものをBoardの上に移動。
constructor() {
super();
this.state = {
history: [
{
squares: Array(9).fill(null),
select: null,
},
],
xIsNext: true,
stepNumber: 0,
isDesc: false, // ← これ
};
}
- ソートの状態を変更する「Game#sortMoves」を追加
sortMoves(isDesc) {
this.setState({isDesc});
}
- Game#renderにて
- にreversedを付けるためにreturn前で
- の定義と慣れ控え
let ol;
if (this.state.isDesc) {
ol = <ol reversed>{moves.reverse()}</ol>
}
else {
ol = <ol>{moves}</ol>
}
- Game#renderのreturnするJSXにol埋め込みとソートボタン表示
<div>
<span>select sort: </span>
<label><input type="radio" onChange={() => this.sortMoves(false)} checked={!this.state.isDesc} />asc</label>
<label><input type="radio" onChange={() => this.sortMoves(true)} checked={this.state.isDesc} />desc</label>
</div>
{ol}
5. When someone wins, highlight the three squares that caused the win.
訳:どちらかが勝った時に、どの手で勝ったか3つのマスをハイライトさせてみましょう。
- チュートリアルで用意されている関数calculateWinnerを改造して勝ちの決め手になった列(line)も返却するようにした
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],
];
const createWinner = (charactor, line) => {
return {charactor, line}
};
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 createWinner(squares[a], lines[i]);
}
}
return createWinner(null, []);
}
- createWinnerのI/Fを変えてしまったので使用箇所を変更
...
if (calculateWinner(squares).charactor || squares[i]) {
return;
}
...
render() {
const history = this.state.history,
current = history[this.state.stepNumber],
squares = current.squares,
winner = calculateWinner(squares);
let status;
if (winner.charactor) {
status = `Winner: ${winner.charactor}`;
}
else {
status = `Next player: ${this.state.xIsNext ? 'X' : 'O'}`;
}
...
- Boardコンポーネントに勝ちの決め手の列を渡す
...
<Board
onClick={(i) => this.handleClick(i)}
squares={squares}
select={current.select}
winLine={winner.line}
/>
...
- Boadコンポーネントはそれを受けてSquareコンポーネントにハイライトをするかどうかの指示を出す。
- 調査中メモ:Propsに空の配列を渡すとプロパティがundefinedになる?
class Board extends React.Component {
renderSquare(i, highLight) {
return <Square
key={i}
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
highLight={highLight} // ←これ
/>;
}
render() {
const rows = [];
for (let y = 0; y < 3; y++) {
const squares = [];
for (let x = 0; x < 3; x++) {
const index = y * 3 + x,
highLight = this.props.winLine ? this.props.winLine.includes(index) : false ;
squares[x] = this.renderSquare(index, highLight);
}
rows.push(
<div key={y} className="board-row">
{squares}
</div>
);
}
return (
<div>{rows}</div>
);
}
}
- Squareコンポーネントはハイライトの指示を受けてクラスを付けたし
function Square(props) {
let className = "square"
if (props.highLight) {
className = `${className} highlight`
}
return (
<button className={className} onClick={() => props.onClick()}>
{props.value}
</button>
);
}
- ハイライト用のCSSクラスを追加
黄色にしました。
.square.highlight {
background: yellow;
}
その他
日本人的感覚で「X」から開始ではなく「O」から開始にしました。
GameのstateのxIsNextの初期値をfalseにするだけです。
感想等
- JavaScriptにもScalaのようなif式、for式がほしい
- 条件が複雑になるとJSX記述で悩むことが多い
- 変数に落とすか、コンポーネント化してしまうか。
最後に
- もう少しエレガントな解があったらコメントなど頂けると嬉しいです。