LoginSignup
1
0

React チュートリアル:三目並べを TypeScript で書いてみた

Posted at

チュートリアル: 三目並べ

サンプルコードが 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 が持つ値は
XOnull しかないことが分かる。
毎回 string | null と書くのはちょっと面倒だし、string だと XO 以外の文字も許容していることになるので、この機会に型を宣言してそれを使い回すのがよさそう。

  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 ファイルが大きくなり、パフォーマンスの問題もあるとか・・・。
詳しい話は、以下のリンクから読めます☆

ですが、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] をどう理解していいか分からなかったけど、nullfalsy であることを利用して 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 で細かくコミットを刻むとロールバックもしやすいし、コミットの数だけ達成感を味わえるので、よき🙌

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0