React Tutorial をTypeScript でやってみた

React のチュートリアルをTypeScript でやってみたのでそれの覚書です。
https://ja.reactjs.org/tutorial/tutorial.html

一応ソースは[_ここ_](https://github.com/shun/react-tutorial)にあります。


前提条件として、yarnはすでに入っている前提です。
ソースコードはprettier で整形した形になっています。

環境は以下になります。

```bash
npm ls --depth=0
my-app@0.1.0
├── @testing-library/jest-dom@5.12.0
├── @testing-library/react@11.2.7
├── @testing-library/user-event@12.8.3
├── @types/jest@26.0.23
├── @types/node@12.20.13
├── @types/react@17.0.8
├── @types/react-dom@17.0.5
├── react@17.0.2
├── react-dom@17.0.2
├── react-scripts@4.0.3
├── typescript@4.3.2
└── web-vitals@1.1.2

```

## チュートリアルの準備

以下のコマンドを実行して環境を作成します。

```bash
$ yarn create react-app my-app --template typescript
```

作成したプロジェクトが起動するか確認します。

```bash
$ cd my-app
$ yarn start
```

ブラウザが立ち上がりいつものReact のロゴが表示されればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65929/dd6976ab-442d-3296-17d0-85a6ce8cdef2.png)

### ローカル環境の準備

src以下のファイルをすべて削除し、index.css、index.tsxを追加します。

```bash
rm -rf src/*
```

```css:index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol,
ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}
```

```tsx:index.tsx
class Square extends React.Component {
  render() {
    return <button className="square">{/* TODO */}</button>;
  }
}

class Board extends React.Component {
  renderSquare(i: number) {
    return <Square />;
  }

  render() {
    const status = "Next player: X";

    return (
      <div>
        <div>
          <div className="status">{status}</div>
        </div>

        <div>
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>

        <div>
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>

        <div>
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>

        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

ReactDOM.render(<Game />, document.getElementById("root"));

```

yarn start を実行して以下のようなのが表示されればOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65929/b3de9fda-301c-010c-0950-9c9fadaf8dcb.png)

### データを Props 経由で渡す

Propsでデータを渡すためにまず型を定義します。

```tsx:index.tsx
type Props = {
  value: number;
};
```

React の Component はジェネリックス使用してして、Propsを渡せるように定義されています。
※ ソースのコメントは削除しています

```ts:@types/react/index.d.ts
    // Base component for plain JS classes
    interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }
    class Component<P, S> {
        static contextType?: Context<any>;

        // TODO (TypeScript 3.0): unknown
        context: any;

        constructor(props: Readonly<P> | P);

        constructor(props: P, context: any);

        setState<K extends keyof S>(
            state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
            callback?: () => void
        ): void;

        forceUpdate(callback?: () => void): void;
        render(): ReactNode;

        readonly props: Readonly<P> & Readonly<{ children?: ReactNode }>;
        state: Readonly<S>;

        refs: {
            [key: string]: ReactInstance
        };
    }
```

ジェネリックスで指定する「P」の部分がconstractor のprops の型として設定されていまので
設定したProps にアクセスしたい場合は、this.props でアクセスするよう先程のソースを以下のように変更します。

```diff
-class Square extends React.Component {
+class Square extends React.Component<Props> {
  render() {
-    return <button className="square">{/* TODO */}</button>;
+    return <button className="square">{this.props.value}</button>;
  }
}
```

Square クラスがProps でvalue を渡せるようになったので、Board クラスもvalue を渡すように変更します。

```diff
class Board extends React.Component {
  renderSquare(i: number) {
-    return <Square />;
+    return <Square value={i} />;
  }
```

ここまでを変更したら、再度yarn startを実行します。
Props で渡されたvalue がマス目に設定されているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65929/af317abf-d8ae-3078-3358-67788a8b44c0.png)

### インタラクティブなコンポーネントを作る

まずは、マス目をクリックした際に、alert を表示するようにします。

```diff
class Square extends React.Component {
  render() {
-    return <button className="square">{/* TODO */}</button>;
+    return (
+      <button className="square" onClick={() => alert("click")}>
+        {this.props.value}
+      </button>
+    );
  }
```

マスをクリックするとalertが表示されることが確認でき、onClickのイベントが実行されたことがわかります。

続いて、マス目がクリックされたことをstate として覚えれるよう、以下のように変更します。
State を保持するために、State 型も定義する必要があります。
State 型を定義した後、Props と同様にジェネリックスに設定します。

```diff
-class Square extends React.Component<Props> {
+type State = {
+  value: string | null;
+};
+
+class Square extends React.Component<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      value: null,
+    };
+  }
+
  render() {
```

これで、マス目をクリックすると、「X」が表示されるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65929/ab2b4f31-3a98-f35b-712f-451caf927ca3.png)

### State のリフトアップ

親コンポーネントであるBoard クラスで各マス目の状態を管理できるように修正します。
まずは、９マスの状態を管理できるようBoardState 型を追加します。

```diff
type State = {
  value: string | null;
};
+
+type BoardState = {
+  squares: string[];
+};
```

BoardState 型を作成したので、Board クラスで使用できるように変更します。
Board クラスでProps は特に使用しないため、{} を渡すようにします。
※ 第１引数（？）がProps で、第２引数（？）がState のため、第１引数に{} を渡しています。
Board クラスのコンストラクタで９マス分のState を空文字で初期化しておきます。

```diff
-class Board extends React.Component {
+class Board extends React.Component<{}, BoardState> {
+  constructor(props: {}) {
+    super(props);
+    this.state = {
+      squares: Array(9).fill(""),
+    };
+  }
```

続いて、マス目がクリックされた時のイベントハンドラをBoard クラスに追加します。

```diff
+  handleClick(i: number) {
+    const squares = this.state.squares.slice();
+    squares[i] = "X";
+    this.setState({ squares: squares });
+  }
+
  renderSquare(i: number) {
```

マス目がクリックされたときのハンドラをBoard クラスに追加したので、Square クラスにProps として
渡せるように型を修正します。

```diff
type Props = {
  value: string;
+  onClick: () => void;
};
```

これで、Board クラスのクリックハンドラをSquare に渡せるようになりました。
マス目の値はBoard クラスが保持しているマス目の値を描画し、マス目がクリックしたときのイベントは
Square → Board に通知され、Board クラスのハンドラでState の変更処理を行います。

```diff
  renderSquare(i: number) {
-    return <Square value={i} />;
+    return (
+      <Square
+        value={this.state.squares[i]}
+        onClick={() => this.handleClick(i)}
+      />
+    );
  }
```

### 関数コンポーネント

ここまでの修正で、Square クラスはState を保持する必要がなくなりました。
ですので、Square クラスを関数コンポーネントに変更します。

```diff
-class Square extends React.Component<Props, State> {
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      value: null,
-    };
-  }
-
-  render() {
-    return (
-      <button className="square" onClick={() => this.props.onClick()}>
-        {this.props.value}
-      </button>
-    );
+function Square(props: Props) {
+  return (
+    <button className="square" onClick={() => props.onClick()}>
+      {props.value}
+    </button>
+  );
 }
```

State 型を参照するコードがなくなったので、不要なState 型を削除します。

```diff
-type State = {
-  value: string | null;
-};
```

### 手番の処理

プレーヤの番手を示す状態をBoardState に追加します。

```diff
type BoardState = {
  squares: string[];
+  xIsNext: boolean;
};
```

先手を「X」にするので初期値にtrue を設定します。

```diff
class Board extends React.Component<{}, BoardState> {
  constructor(props: {}) {
    super(props);
    this.state = {
      squares: Array(9).fill(""),
+      xIsNext: true,
    };
  }
```

クリックされたときのハンドラで番手を反転させるようにします。

```diff
   handleClick(i: number) {
     const squares = this.state.squares.slice();
-    squares[i] = "X";
-    this.setState({ squares: squares });
+    squares[i] = this.state.xIsNext ? "X" : "O";
+    this.setState({ squares: squares, xIsNext: !this.state.xIsNext });
   }
```

処理自体、プレーヤを反転できるようになったので、プレーヤの状態も更新刷るようにします。

```diff
   render() {
-    const status = "Next player: X";
+    const status = "Next player: " + (this.state.xIsNext ? "X" : "O");

     return (
       <div>
```

### ゲーム勝者の判定

ゲームの勝利判定の関数を追加します。
lines には同じマークが３つ並んだら勝ちとなるマスのインデックスの組み合わせを登録します。

```diff
+function calculateWinner(squares: string[]): string {
+  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 "";
+}
```

あとは、クリック時に勝者判定を追加します。

```diff
  handleClick(i: number) {
    const squares = this.state.squares.slice();
+    if (calculateWinner(squares) || squares[i]) {
+      return;
+    }
    squares[i] = this.state.xIsNext ? "X" : "O";
```

```diff
-    const status = "Next player: " + (this.state.xIsNext ? "X" : "O");
+    const winner = calculateWinner(this.state.squares);
+    let status;
+    if (winner) {
+      status = `Winner: ${winner}`;
+    } else {
+      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
+    }
```

とりあえず、一通りのチュートリアルはここまでで完成かと思います。
TypeScript で書くとこんな感じかなと思います。。。

長くなりましたが、ここまでお付き合いしていただきありがとうございました。