チュートリアル: 三目並べ
サンプルコードが JavaScript なので TypeScript で書きながら勉強してみた。
概要
スタートコードの確認
export default function Square() {
return <button className="square">X</button>
}
default キーワードは、このコードを使用する他のファイルに、これがこのファイルのメイン関数であるということを伝えます。
ブルーベリー本にも
モジュールからエクスポートされる代表的な値を表すために default という名前が使用され、それを扱うために専用の構文が用意されていると理解しましょう。
って書いてある。
そしてブルーベリー本のコラムには default
キーワードを使うことをおすすめしてない印象があります。
理由は、
エディタによるサポートの弱さ
を挙げています。
おすすめは、
エクスポートする変数名を決めるときはそのままインポートできる名前にすべきである
とのことです。
正直エディタによるサポートの弱さ(自動補完されない)をまだ感じたことはないけど、「エクスポートする変数名を決めるときはそのままインポートできる名前にすべき」はそりゃそうだなって思います。default
キーワードを使うとインポート側で変数名を自由に変えることができるから、もしインポート側で変えちゃったら可読性は落ちるよね〜。と思いつつ、インポート側で変数名を変えないことを前提に、default
キーワードを目印代わりに使うのは悪くなさそうだな〜というのが、今の考えです。
あぁ・・・。ここ React じゃない話だ・・・。
盤面の作成
この章の最終コード
github.com/b-yuko/react-tutorial-tic-tac-toe-typescript/「盤面の作成」が完了したコード
props を通してデータを渡す
props を渡すところで型を指定しないと、怒られます・・・
TS7031: Binding element value implicitly has an any type.
- function Square({ value }) {
+ function Square({ value }: { value: string }) {
return <button className="square">{ value }</button>;
}
この章の最終コード
github.com/b-yuko/react-tutorial-tic-tac-toe-typescript/「props を通してデータを渡す」の最終コード
インタラクティブなコンポーネントの作成
state が持つ値の型を書く。
初期値は null なのに、set する値が string だから怒られます・・・
TS2345: Argument of type "X" is not assignable to parameter of type SetStateAction<null>
function Square() {
- const [value, setValue] = useState(null)
+ const [value, setValue] = useState<string | null>(null)
function handleClick() {
setValue('X')
}
return (...)
}
この章の最終コード
github.com/b-yuko/react-tutorial-tic-tac-toe-typescript/「インタラクティブなコンポーネントの作成」の最終コード
React Developer Tools
Chrome にも拡張機能があって、入れてはみたもののあんまり活用できてない感が否めない・・・。
ゲームを完成させる
state のリフトアップ
state を配列で持つ場合の書き方。
[]
の書く位置を間違えたり、
string | null
を ()
で囲うのを忘れたりして、書き方違うぜ〜って赤波線出てイライラしちゃうんだよね。
export default function Board() {
- const [squares, setSquares] = useState(Array(9).fill(null))
+ const [squares, setSquares] = useState<(string | null)[]>(Array(9).fill(null))
return (...)
}
チュートリアルを読み進めていると、state が持つ値は
X
か O
か null
しかないことが分かる。
毎回 string | null
と書くのはちょっと面倒だし、string
だと X
と O
以外の文字も許容していることになるので、この機会に型を宣言してそれを使い回すのがよさそう。
import { useState } from "react";
+ type SquareValue = "X" | "O" | null;
- function Square({value}) {
+ function Square({ value }: { value: SquareValue }) {
return <button className="square">{value}</button>;
}
export default function Board() {
- const [squares, setSquares] = useState<(string | null)[]>(Array(9).fill(null))
+ const [squares, setSquares] = useState<SquareValue[]>(Array(9).fill(null));
return (...);
}
useState の宣言で (Array(9).fill(null))
こんなふうに初期化するところは初めて見たかも!
配列の中身の個数が決まってるときに使うのかな。
Square
が取る props が複数になったので、interface で型を宣言したのと、
handleClick
が引数を取るので、引数の型を宣言した。
- import { useState } from "react";
+ import React, { useState } from "react";
type SquareValue = "X" | "O" | null;
+ interface SquareProps {
+ value: SquareValue;
+ onSquareClick: React.MouseEventHandler<HTMLButtonElement>;
+ }
- function Square({ value }: { value: SquareValue }) {
+ function Square({ value, onSquareClick }: SquareProps) {
- return <button className="square">{value}</button>;
+ return (
+ <button className="square" onClick={onSquareClick}>
+ {value}
+ </button>
+ );
}
export default function Board() {
const [squares, setSquares] = useState<SquareValue[]>(Array(9).fill(null));
+ function handleClick(i: number) {
+ const nextSquares = squares.slice();
+ nextSquares[i] = "X";
+ setSquares(nextSquares);
+ }
return (...);
}
個人的にオブジェクト型を宣言するときは、interface で宣言しています。
なぜかというと、interface はオブジェクト型しか宣言できないので、interface と書いてあれば「あ〜オブジェクト型なのね」と、すぐに理解できるからです。
他にも、型エイリアス宣言をすると .d.ts
ファイルが大きくなり、パフォーマンスの問題もあるとか・・・。
詳しい話は、以下のリンクから読めます☆
- インターフェースと型エイリアスの使い分け(サバイバル TypeScript)
- 型リテラルのエイリアスよりもインターフェイスを優先する(Google TypeScript Style Guide)
- TypeScript: インターフェイスを優先する(Nicholas Jamieson’s personal blog.)
-
TypeScript の大規模導入から得た 10 の洞察(Bloomberg)
ですが、interface 派と type 派がいること、それぞれの思いを持って使い分けていることも分かるので、個人的に interface ってだけで、どっちでもいいかな〜って思います(笑)
React では、イベントを表す props には onSomething という名前を使い、それらのイベントを処理するハンドラ関数の定義には handleSomething という名前を使うことが一般的です。
そうだったんだ・・・。
何の値を props として受け取っているのか、何のための関数なのか、こうした慣習に従うことで認知的負荷を下げられるなら、活用してもよさそう。
この章の最終コード
github.com/b-yuko/react-tutorial-tic-tac-toe-typescript/「state のリフトアップ」の最終コード
なぜイミュータビリティが重要なのか
- 機能実装が容易
- アクションの取り消しややり直しを簡単に実装できる
- データの過去バージョンを保持し、再利用できる
- パフォーマンス向上
- イミュータビリティにより、データが変更されたかどうかを容易に比較でき、不要な再レンダーを避けられる
- React の memo API の利用で、再レンダーの最適化が可能
手番の処理
新しく useState が追加されました。
初期値として true
が入っているため、型推論してくれるみたいです。
だけど型を書く☆
export default function Board() {
- const [xIsNext, setIsNext] = useState(true)
+ const [xIsNext, setIsNext] = useState<boolean>(true)
const [squares, setSquares] = useState<SquareValue[]>(Array(9).fill(null))
function handleClick(i: number) {...}
}
この章の最終コード
github.com/b-yuko/react-tutorial-tic-tac-toe-typescript/「手番の処理」の最終コード
勝者の宣言
関数を宣言したときの引数はどんな型なのか?は書く必要があります。
あとは let で変数名だけを宣言している箇所も、あとから文字を代入してるけど、宣言時にどんな型が入るかも合わせて宣言するとよさそう。
export default function Board() {
const [squares, setSquares] = useState<SquareValue[]>(Array(9).fill(null));
const [xIsNext, setXIsNext] = useState<boolean>(true);
function handleClick(i: number) {...}
const winner = calculateWinner(squares);
- let status;
+ let status: string;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
return (...);
}
- function calculateWinner(squares) {
+ function calculateWinner(squares: SquareValue[]) {
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];
}
}
return null;
}
calculateWinner
関数の if 文の squares[a] && squares[a]
をどう理解していいか分からなかったけど、null
が falsy
であることを利用して null
が入っていたら if 文から抜けるように作っているのね。
まだまだ JavaScript が読めないなんて。。。_| ̄|○
この章の最終コード
github.com/b-yuko/react-tutorial-tic-tac-toe-typescript/「勝者の宣言」の最終コード
タイムトラベルの追加
着手の履歴を保持する
- 過去の squares 配列を、history という配列に保存する
- history 配列は、最初の着手から最新の着手まで、盤面のすべての状態を表現する
もう一度 state をリフトアップ
onPlay みたいな関数の型を定義するとき、いつもどう書くか分からなくなる・・・。まだ慣れない・・・。
関数の引数と引数の型、戻り値の型を書く。戻り値がない場合は void 。
+ interface BoardProps {
+ xIsNext: boolean;
+ squares: SquareValue[];
+ onPlay: (nextSquares: SquareValue[]) => void;
+ }
- function Board({ xIsNext, squares, onPlay }) {
+ function Board({ xIsNext, squares, onPlay }: BoardProps) {
function handleClick(i: number) {...}
...
return (...);
}
export default function Game() {
const [xIsNext, setXIsNext] = useState<boolean>(true);
- const [history, setHistory] = useState([Array(9).fill(null)]);
+ const [history, setHistory] = useState<SquareValue[][]>([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
- function handlePlay(nextSquares) {
+ function handlePlay(nextSquares: SquareValue[]) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
Board コンポーネントに set 関数を渡さずに、set 関数を実行する関数(handlePlay)を作って渡してる。
私が何も考えずに実装すると、state と set 関数の両方を Board コンポーネントに渡しちゃうな・・・。もしそうすると props を4つも渡さないといけなくなるから、あんまりスマートじゃないのかも・・・。
state を更新するタイミングが同じならこうやって関数にまとめて、関数を渡すだけにするとスマートに書けるのね〜という学び。
この章の最終コード
github.com/b-yuko/react-tutorial-tic-tac-toe-typescript/「もう一度 state をリフトアップ」の最終コード
過去の着手の表示
map で button などの要素を量産することができるのを知ったとき、ちょっと衝撃的だった気がする・・・。
map で回した結果を変数に入れて使ってるの新鮮!いつも変数に入れずにそのまま使ってる。どっちがいいんだろう??
export default function Game() {
...
function handlePlay(nextSquares: SquareValue[]) {...}
function jumpTo(nextMove) {
// TODO
}
const moves = history.map((squares, move) => {
- let description;
+ let description: string;
if (move > 0) {
description = "Go to move #" + move;
} else {
description = "Go to game start";
}
return (
<li>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (...);
}
Warning: Each child in a list should have a unique "key" prop. Check the render method of
Game
.
想定通りの Warning が出ました☆
この章の最終コード
github.com/b-yuko/react-tutorial-tic-tac-toe-typescript/「過去の着手の表示」の最終コード
key を選ぶ
- key は React に特別に予約されたプロパティ
- index を key として使うと、リストの項目を並べ替えたり、挿入、削除するときに問題が生じる
- key はグローバルに一意である必要はないが、コンポーネントとその兄弟間で一意である必要がある
タイムトラベルの実装
ずっと赤い波線が出てる箇所があって気になってたんだけど、
const moves = history.map((squares, move) =>
の squares
って使ってないのね。
だけど、第2引数の index になる部分を使いたいから、省略できないし書いてたのか・・・。
アンダースコアにすると、TS6133: squares is declared but its value is never read.
が消えるから、使わないけど省略できないときは _
を使うとよさそう。
export default function Game() {
const [xIsNext, setXIsNext] = useState<boolean>(true);
const [history, setHistory] = useState<SquareValue[][]>([
Array(9).fill(null),
]);
- const [currentMove, setCurrentMove] = useState(0);
+ const [currentMove, setCurrentMove] = useState<number>(0);
const currentSquares = history[currentMove];
function handlePlay(nextSquares: SquareValue[]) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
setXIsNext(!xIsNext);
}
- function jumpTo(nextMove) {
+ function jumpTo(nextMove: number) {
setCurrentMove(nextMove);
setXIsNext(nextMove % 2 === 0);
}
- const moves = history.map((squares, move) => {
+ const moves = history.map((_, move) => {
let description: string;
if (move > 0) {
description = "Go to move #" + move;
} else {
description = "Go to game start";
}
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return(...);
}
この章の最終コード
github.com/b-yuko/react-tutorial-tic-tac-toe-typescript/「タイムトラベルの実装」の最終コード
最後のお掃除
- state に格納するものを単純化すると
- バグが減る
- コードが理解しやすくなる
この章の最終コード
github.com/b-yuko/react-tutorial-tic-tac-toe-typescript/「最後のお掃除」の最終コード
おわりに
React のチュートリアルがあるのは知ってたけど、やったことはなかったので、色々気づきがあった。やってよかったよかった🥳
REACT を学ぶ ってところでも基礎的なことが学べそうなので、次はそっちをやってみようと思います!
git で細かくコミットを刻むとロールバックもしやすいし、コミットの数だけ達成感を味わえるので、よき🙌