Reactのチュートリアルが新しくなったので4年ぶりにやってみた
はじめに
reactのチュートリアルが新しくなったようなので↓、Viteを使ってやってみることにしました。
↓が自分が昔やった、チュートリアルです。確かにuseStateが使われて無いですね。
しかもGitHub Pagesで公開されていますね。すっかり記憶の彼方ですが。セットアップ
まずは、npm create vite@latest
を実行します。構成は↓です。
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC
もちろんreactのチュートリアルは、typescriptじゃ無いので、ところどころcopilotさんの力を借ります。
それと、もともとviteのテンプレートとして存在したcssは削除して、三目並べのcssを持ってきます。
※チュートリアルに埋め込まれてるcodesandboxをForkなどして、別のブラウザで開いてコピペ
実際にやってみる
↓あたりまでは、内容が簡単なので一気に飛ばします。
そして、ここで、Square コンポーネントを下記のように編集するんですが、暗黙のAny型になってるのでエラーが出てます。
↓あたり読めばなんとなくわかる気がしますが、npm run dev
で実行中は、implicitly has an 'any' type
が出てても実行できます。
ですが、buildするとちゃんとエラーが出てくれます。
$ npm run build
> vite-practice@0.0.0 build
> tsc && vite build
src/App.tsx:3:18 - error TS7031: Binding element 'value' implicitly has an 'any' type.
3 function Square({value}) {
~~~~~
自分は、typescriptの書き方はより厳密に行きたいタイプなので、エラーを修正したいと思います。
とは言え、自分は普段react書かないので、関数の引数で分割代入が行われるときの型指定とか、覚えてません。
こういう時は、copilotさんにお願いします↓
webフロント系は、結構役に立ってくれる印象があります。
まあでも、落ち着いて、ドキュメントをよく見ればこの辺りに書いてありました。
こうなるとPropsのinterfaceも定義したくなります。
interface SquareProps {
value: string | null;
onSquareClick: () => void;
}
function Square({value, onSquareClick}: SquareProps) {
return <button className="square" onClick={onSquareClick}>{value}</button>;
}
こんな感じになるかと思います。
ちょっと話がそれますが、AWS CDKのPropsを定義するときに似てますね(同じ?)。
Terraformを使うことの方が多いですが、なんでも使ってみるもんだなと思いました。
そしてそうこうすると
まで進んで、とりあえずゲームが出来上がります。ここでこんな気になるコードがありました。
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
return (
<>
<div className="status">{status}</div>
// 以下略
letの部分が気になるんですが、2024年現在、まだif式みたいなものは導入されてないようです。
IIEFというのを使って、こんな書き方もあるようです。
こういう書き方は嫌いじゃないんですが、可読性が上がってるか微妙だったので、これくらいなら三項演算子でいいかなと思いました。ついでにコンポーネント化してみました↓
あとはタイムトラベル機能を作れば完成です。
Boardに3つのpropsを受け入れられるようにするので、ここもinterfaceを作らないと読みづらそうです。
interface BoardProps {
xIsNext: boolean;
squares: (string | null)[];
onPlay: (squares: (string | null)[]) => void;
}
function Board({ xIsNext, squares, onPlay }: BoardProps) {
// ...
}
それと、1手目から最終手までの全ての盤面を記録するhistoryというstateも型が明示されてないと若干わかりづらかったので、こんな風にしました。
type History = (string | null)[][];
export default function Game() {
const [history, setHistory] = useState<History>([Array(9).fill(null)]);
// ...
}
追加実装
これだけだと物足りないので、追加の機能を実装して行きます。
現在の着手の部分だけ、ボタンではなく “You are at move #…” というメッセージを表示するようにする。
の部分ですが、こんな感じかと思います。
export default function Game() {
// ...
const moves = history.map((_, move) => {
const descriptionButton = (move > 0)?
'Go to move #' + move:
'Go to game start';
const description = (move === 0)?
'You are at Game start':
'You are at move #' + move;
return (
<li key={move}>
{move === currentMove ?
(<b>{description}</b>) :
(<button onClick={() => jumpTo(move)}>{descriptionButton}</button>)
}
</li>
);
})
}
マス目を全部ハードコードするのではなく、Board を 2 つのループを使ってレンダーするよう書き直す
これは最初こんな感じで、やってみました。
const boardRows =
[0, 1, 2].map((i) => {
return (
<div key={i} className="board-row">
{[0, 1, 2].map((_, j) => (
<Square
key={j + 3*i}
value={squares[j + 3*i]}
onSquareClick={() => handleClick(j + 3*i)} />
))}
</div>
);
});
return (
<>
<CurrentGameStatus winner={winner} xIsNext={xIsNext} />
{boardRows}
</>
);
}
でも、これでいい気がしてきました。
.board-container {
width: fit-content;
display: grid;
grid-template-columns: repeat(3, 1fr);
}
return (
<>
<CurrentGameStatus winner={winner} xIsNext={xIsNext} />
<div className="board-container">
{
Array.from({ length: 9 }, (_, i) =>
<Square
key={i}
value={squares[i]}
onSquareClick={() => handleClick(i)} />)
}
</div>
</>
);
ここまでのソース
記事の分量がいい感じになりましたので、一旦ここで区切りたいと思います。
Discussion