React公式サイトには入門に最適なチュートリアルが用意されています。
マルバツゲームの作成を通して、Reactとその構文について学ぶことができるようになっています。
実際に手を動かしながら学ぶことができるので、一段階ずつ学べるガイドと合わせてスムーズにReactに入門できるような内容です。
公式チュートリアルはこちらになります。
https://ja.reactjs.org/tutorial/tutorial.html
今回は公式チュートリアルの最後に載っている「改良のアイデア」を実装してみました。
ただ、前の記事で指摘したように、公式チュートリアルは実装方法が古いです。
なので、今回もTypeScriptと関数コンポーネントで実装しました。
こちらの記事を読んでいない方はまずこちらの記事からリファクタリングをどうぞ。
まだ時間がある場合や、今回身につけた新しいスキルを練習してみたい場合に、あなたが挑戦できる改良のアイデアを以下にリストアップしています。後ろの方ほど難易度が上がります。
と公式には書かれています。
確かにさらにスキルを伸ばす上ではかなり良い題材でした。
しかし、個人的には全く難易度順では無いと思いましたので、記事内に個人的な難易度も記載しておきます。
では、それぞれ順に解説をしていきます。
Reactエンジニアを目指している方へ、
知識0からReactエンジニアになるまでのロードマップをまとめました。
こちらも記事もぜひ参考にどうぞ。
スクールでReactを深く学びたい方へ、
Reactを学べるおすすめのプログラミングスクールもまとめてますので、こちらもどうぞ。
Reactのおすすめ教材が知りたい方へ、
現役のReactエンジニアが>Reactのおすすめ教材をまとめました。
こちらも記事もぜひ参考にどうぞ。
- 文系学部(体育学部)卒だけど、Webエンジニアに転職成功
- 現役のReactエンジニア
- 実務10ヶ月でフリーランスとして独立
- 元々勉強が苦手で、大学の偏差値は50ほど
- 血液型はO型、千葉県出身の神奈川県在住、8月生まれの現在26歳
その1:履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する
難易度4
タイムトラベル用のボタンにcol(縦のマス目)、row(横のマス目)の番号を表示するような機能になります。
実装方法としては、
- colAndRowsというマス目番号の配列を状態としてコンポーネントに持たせる
- それを番号順にボタンの中に表示する
という感じです。
まずは、useStateを使って新しいstateを宣言します。
次に、クリックされたマス目ごとに対応するcolとrowの番号を変数に持たせます。
後は、タイムトラベル機能にも対応して、stateにそれらを設定すれば、第一段階は完了です。
useGameControl.tsは以下のようになります。
import {useState} from "react";
import {historyType} from "../types/historyType";
import {calculateWinner} from "../utils/calculateWinner";
export const useGameControl = () => {
const [history, setHistory] = useState<Array>([{squares: Array(9).fill(null)}])
const [stepNumber, setStepNumber] = useState(0);
const [xIsNext, setXIsNext] = useState(true);
const [colAndRows, setColAndRows] = useState<Array<Array>>([]);
const handleClickSquare = (i: number) => {
const copyedHistory = history.slice(0, stepNumber + 1);
const current = history[copyedHistory.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
let nextColAndRow:Array = [];
if(i === 0){
nextColAndRow = [0,0];
}else if(i === 1){
nextColAndRow = [0,1];
}else if(i === 2){
nextColAndRow = [0,2];
}else if(i === 3){
nextColAndRow = ;
}else if(i === 4){
nextColAndRow = ;
}else if(i === 5){
nextColAndRow = ;
}else if(i === 6){
nextColAndRow = [2,0];
}else if(i === 7){
nextColAndRow = [2,1];
}else if(i === 8){
nextColAndRow = [2,2];
}
let slicedColAndRows = colAndRows.slice(0, stepNumber);
slicedColAndRows.push(nextColAndRow);
setColAndRows(slicedColAndRows);
squares[i] = xIsNext ? "X" : "O";
setHistory(copyedHistory.concat([
{
squares: squares
}
]));
setStepNumber(copyedHistory.length);
setXIsNext(!xIsNext);
}
const jumpToPast = (step: number) => {
setStepNumber(step);
setXIsNext((step % 2) === 0);
}
return {history, stepNumber, xIsNext, colAndRows, handleClickSquare, jumpToPast};
}
最後に、Gameコンポーネント側で新しく作成した、colsAndRowsを受け取り、ボタン内にそれらを表示すれば完成です。
Gameコンポーネントは以下のようになります。
import React, {VFC} from 'react';
import {Board} from "./Board";
import {useGameControl} from "../hooks/useGameControl";
import {calculateWinner} from "../utils/calculateWinner";
export const Game: VFC = () => {
const {history, stepNumber, xIsNext, colAndRows, handleClickSquare, jumpToPast} = useGameControl();
const handleClick = (i: number) => handleClickSquare(i);
const jumpTo = (step: number) => jumpToPast(step);
const current = history[stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move + "(" + colAndRows[move - 1][0] + ", " + colAndRows[move - 1] + ")":
'Go to game start';
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={i => handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
その2:着手履歴のリスト中で現在選択されているアイテムをボールドにする
難易度2
こちらは、現在表示している着手のタイムトラベルボタンの文字をボールド(太字)にする機能になります。
実装方法としてはめちゃくちゃ簡単でstepNmuberで条件分岐を作るだけで実現できます。
React内でCSSを実装する方法はいくつかあるのですが、今回は一番オーソドックスなInline Stylesという方法で実装します。
実装方法を説明すると、CSSのプロパティ名と値を対応させたJSのオブジェクト作成して、CSSを当てたいコンポーネントにstyleとして指定すればOKです。
この機能を実装したGameコンポーネントの一部は以下のようになります。
const boldStyles = {
fontWeight: 700
};
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move + "(" + colAndRows[move - 1][0] + ", " + colAndRows[move - 1] + ")":
'Go to game start';
return (
<ul>
<li>{move === stepNumber ? <button> jumpTo(move)} style={boldStyles}>{desc}</button> :
<button> jumpTo(move)}>{desc}</button>
</li>
</ul>
);
});
CSSを別ファイルで管理したりする現場が多いかと思いますが、今回はここしかInline Stylesは使用していないので、同じファイルに置いたままにします。
その3:Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える
難易度3
こちらは、renderSquareを9回繰り返していて冗長になっている所を、ループを使うことで短く表現するような実装になります。
実装方法としては簡単なのですが、どう実現するか考えるのが結構大変なので、難易度は3にしてあります。
実装手順としては、
- 対応するColとRowの配列を準備する
- それらを使用して2重ループを作る
という手順になります。
では、実装していきましょう。
まず、最初にPropsを分割代入しておきましょう。
const {squares, onClick} = props;
これをすることで、いちいちprops.hogeと打たずにhogeだけでPropsの値を扱えるようになります。
結構使っている現場も多いと思いますので、覚えておきましょう。
そして、rowsとcolsの二つの配列を準備します。
それらを使って2重ループを実装すれば完了です。
rowsをmapで回してboard-rowのdivタグを3つ作成し、colsを使ってrenderSquareにそれぞれ対応する値を与えてやればOKです。
ただ、公式のチュートリアルにあったようにリストにはしっかりとKeyを指定しないとコンソールエラーになるので、指定しておきましょう。
実装後のBoardコンポーネントは以下のようになります。
export const Board: VFC<boardProps> = (props) => {
const {squares, onClick} = props;
const renderSquare = (i: number) => {
return (
<Square
value={squares[i]}
onClick={() => onClick(i)}
key={i}
/>
);
}
const cols = [0,3,6];
const rows = [0,1,2];
return (
<div>
{rows.map(row =>
<div className="board-row" key={row}>
{cols.map( (square, index) => renderSquare(index + cols[row]))}
</div>
)}
</div>
);
}
その4:着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する
難易度5
さて、1番の難題になります。
ここがかなり難しいですが、残り2つは簡単なので、気張っていきましょう。
まず、これはタイムトラベル用のボタンを降順・昇順入れ替えられるようにするトグルボタンの実装なります。
一見簡単そうに見えるのですが、考慮することが多くあるので、かなり大変です。
実装手順としては、
- 降順と昇順を扱えるようにするreverseFlgという新たなstateとそれを入れ替えるための関数を用意する
- Gameコンポーネント内に降順用の配列を用意する
- 降順にも対応した表示やタイムトラベルに変更する
になります。
まずは、useGameControl.ts内にフラグとトグルボタン用の関数を用意します。
const [reverseFlg, setReverseFlg] = useState(false);
const reverseHistoryInf = () => {
setReverseFlg(!reverseFlg);
}
そして、game-infoのdivタグの中にトグルボタンを実装します。
<button onClick={reverseHistoryInf}>Reverse History Order</button>
次に、Gameコンポーネント内に、降順にする用の配列を実装をします。
const reverseArr: Array = [];
for(let i=history.length -1; i >= 0; i--){
reverseArr.push(i);
}
そして、moves内をフラグのtrue、falseそれぞれに対応した実装にします。
const moves = history.map((step, move) => {
let number = reverseFlg ? reverseArr[move] : move;
const desc = (reverseFlg === false && move) || (reverseFlg && move !== history.length -1) ?
`Go to move #${number}(${colAndRows[number - 1][0]}, ${colAndRows[number - 1]})`:
'Go to game start';
return (
<li key={move}>
{number === stepNumber ? <button style={boldStyles}>{desc}</button> :
<button onClick={() => jumpTo(number)}>{desc}</button>
}
</li>
);
});
これで、タイムトラベルや先ほど実装したBoldなどにも対応した状態になりました。
その5:どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする
難易度3
これは比較的簡単でして、calculateWinner関数を書き換えて、勝利を決定したマス目の配列を返すようにし、そのマス目にcssを当てれば完成です。
まず、calculateWinnerを書き換えて対象の配列を返すようにしましょう。
returnをsquares[a]から[a,b,c]に変更するだけです。
次に、Gameコンポーネント内にある、勝者を格納する所を少し書き換えます。
if (winner) {
status = "Winner: " + current.squares[winner[0]];
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
winner = []
}
そして、calculateWinner関数から返ってきた配列をpropsとしてBoardコンポーネントに渡します。
boardPropsも書き換える必要があるので、そこもお忘れなく。
そして、Boardコンポーネント内で対応するSquareには背景色をつけるようのstyleをpropsとして渡します。
const winnersStyle = {backgroundColor: "yellow"}
const renderSquare = (i: number) => {
if(winRow.some(value => value === i)){
return (
<Square
value={squares[i]}
onClick={() => onClick(i)}
key={i}
style={winnersStyle}
/>
);
}else{
return (
<Square
value={squares[i]}
onClick={() => onClick(i)}
key={i}
/>
);
}
}
こちらも、squareTypeを書き換えるようにしてください。
そして、square関数にstyleを設定してやれば完成です。
export const Square: VFC<squareProps> = (props) => {
return (
<button className="square" onClick={props.onClick} style={props.style}>
{props.value}
</button>
);
}
その6:どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する
難易度1
最後になります。
こちらは結果が引き分けの時に、勝者や指し手を表示している箇所にDrawと表示するようにします。
実装方法としては、
- calculateWinner関数内で、全部のマス目が埋まっておりかつ勝敗が決まっていない時に10という数字が入った配列を返すように変更する
- 10が入った配列が返ってきた時は、Drawと表示する
以上になります。
ではまず、calculateWinner関数を書き換えましょう。
export const calculateWinner = (squares: Array<oneSquareType>) => {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
,
[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 [a,b,c];
}
}
for (let i=0; i < squares.length; i++){
if(!squares[i]){
return null
}
}
return [10]
}
次に、Gameコンポーネント内の勝者指し手を表示している箇所を書きます。
let status;
if(winner === null){
status = "Next player: " + (xIsNext ? "X" : "O");
winner = [];
} else if(winner[0] === 10){
status = "Draw";
winner = [];
}else {
status = "Winner: " + current.squares[winner[0]];
}
これで、完成です。
ちなみに、Drawの時に返している数字が10なのは特に理由はありません。
単に、TypeScript的に数字の配列を返さないといけないので、Drawとわかるように通常では絶対に返さない値を返しているだけです。
公式チュートリアル的には、これが一番難しい追加機能らしいのですが、絶対に違いますね。謎です、、
まとめ 【応用】Reactチュートリアル改良のアイデアを実装してみた
チュートリアルにこのようなアウトプットの題材が書かれているのはかなり良いですね。
正直、まだ良い書き方が全然あると思いますので、そこも考えつつさらにリファクタリングすれば、スキルをもう1段階上げることができるかと思います。
また、わからなかった所などがありましたら、こちらのリポジトリを参考にしてみてください。
https://github.com/hinoharashinya/react-tutorial-add-function
最後まで、読んでいただきありがとうございました。
僕のブログでは他にもプログラミングについての解説記事もありますので、気になる方は見てみてください。
Reactエンジニアになりたい人はこちらからどうぞ
Reactを学べるおすすめのプログラミングスクールを知りたい方はこの記事をどうぞ。
Reactのおすすめ教材を知りたい人はこの記事をどうぞ。
「プログラミングスクールが高くて通えない。。」
といった悩みがこのサービスで解決します。
それは、次世代型のサブスクプログラミングスクールになります。
具体的に、このスクールは以下のことが可能です。
- 講師とのマンツーマンレッスン
- 質の高いかなりボリュームのある教材
- 講師に質問し放題
そして、お値段はたったの1,980円から。
これで高いお金を払わずに、エンジニアになることが可能です。
今なら、全額返金保証もあります。
エンジニアを目指す人も年々増えているで、お早めにどうぞ。
当サイト限定の、初月50%OFFクーポン(HINOSHIN)あり
>>侍テラコヤの評判・口コミ|現役エンジニアが実際に使ってみた感想