TypeScriptでReactに入門するチュートリアル

# はじめに
## 本投稿の背景と目的
[Reactの公式チュートリアル](https://ja.reactjs.org/tutorial/tutorial.html)は大変よくできているので、初学者がReactの基本を学ぶためには、まずは公式チュートリアルに沿って手を動かしていくのが一番だと思います。
私自身、公式チュートリアルをやってみて基本的な部分の理解はできたものの、以下の点が自分にとっては課題に感じました。

* TypeScriptで書きたい場合にどうすればよいか分からず、いろいろ調べる必要があった
* 三目並べという題材が若干トリッキーに感じた

というわけで、TypeScriptを使ってReactに入門するための、よりシンプルな題材のチュートリアルを作成することが本投稿の目的です。
私自身が初学者なので、初学者目線での説明を心がけていますが、わかりにくい箇所や間違っている箇所などありましたら忌憚のないコメントを頂けると有り難いです。

（2020-09-22追記）
[TypeScriptでReact Hooksに入門するチュートリアル](https://qiita.com/yonetty/items/ad19bddc26806bb49cef)も投稿しましたので、本チュートリアルを終えた後はそちらもご参照頂けると幸いです。
（2020-10-11追記）
「次に学ぶこと」の章を追加しました。

## 前提とする知識
* HTMLの基本知識
* TypeScriptの基本知識

## 開発環境
チュートリアル本編は、CodePenを使ってブラウザ上でステップ・バイ・ステップでアプリケーションを開発していく流れとなっています。
ローカル開発環境で動かしたい場合は、`(Appendix) ローカル開発環境のセットアップ`を参考にしてください。

## 動作確認済みの環境
* `Mac OS X 10.15.2`
* `node v11.10.1`
* `npm 6.12.0`
* `react 16.12.0`
* `typescript 3.7.4`

# これから作るもの
このチュートリアルでは、とあるテーマパークの入場料計算を行うWebアプリケーションを作成します。
以下のような、カテゴリごとの人数を入力すると合計の人数と金額を計算するシンプルな画面です。

<img width="434" alt="react-ex-01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176589/16d20dc6-c664-2b69-917d-15f776107153.png">

最終的な結果をここで確認することができます：[最終結果](https://codepen.io/tyonekubo/pen/KKwXRpJ?editors=0010)
チュートリアルを進める前に実際に画面を操作して動作を把握しておくことをお勧めします。

# スターターコードの中身を確認する
ブラウザから、この[スターターコード](https://codepen.io/tyonekubo/pen/jOEGxqd?editors=0010)を新しいタブで開いてください。
スターターコードではコンポーネントの雛形が用意してあり、ウィンドウ下部のペインには以下にような単純な(動作しない)画面が表示されているはずです。

<img width="437" alt="react-ex-02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176589/9a3839da-89ec-a370-2a5c-bf3248f6ece2.png">

コードを見てみると、以下の4つのReactコンポーネントがあることがわかります。`コンポーネント`とは、UIを組み立てるための、小さく独立した部品を指します。

* `Detail`（入力用の明細）
* `Summary`（合計を表示）
* `AdmissionFeeCalculator`（明細、合計をまとめたもの）
* `App`（最上位のコンポーネント）

`Summary`のコードを見てみましょう。
コンポーネントは`React.Component`を継承したクラスとして作成し、`render`メソッドを実装します。`render`メソッドでは、`JSX`と呼ばれる構文で記述したDOMの表現を返却します。
JSXは基本的にHTMLの構文で記述することができますが、以下の点は注意してください。

* 最上位の要素は一つでなければならない（以下のコードサンプルでは`div`要素）
* `class`属性はTypeScriptの予約語と重複するため、`className`と書く必要がある（他にも`label`要素の`for`属性は`htmlFor`と書く、等）

```react
class Summary extends React.Component {
  render() {
    return (
      <div>
        <div className="party">
          <input type="text" className="party" value="0" />
          <span>名様</span>
        </div>
        <div className="total-amount">
          <span>合計</span>
          <input type="text" className="total-amount" value="0" />
          <span>円</span>
        </div>
      </div>
    );
  }
}
```

コンポーネントは入れ子の構造にすることが可能です。以下のコードサンプルのように、コンポーネントのクラス名がそのままJSXにおける要素名となります。

```react
class AdmissionFeeCalculator extends React.Component {
  render() {
    return (
      <>
        <Detail />
        <Summary />
      </>
    );
  }
}
```

最上位の要素は一つでなければならないという制約がありました。もし複数の要素を返したい場合は、それらを束ねる`div`タグを上位に配置するという方法も可能ですが、`React.Fragment`要素を用いるとDOMに余計な要素を加えることなく複数の要素を束ねることができます。上記のコードサンプルでは、`React.Fragment`のエイリアスにあたる`<></>`を使用しており、その配下に複数の要素を並べています。

# コンポーネントが必要とするデータをprops経由で渡す
では手始めに、`AdmissionFeeCalculator`コンポーネントから`Detail`コンポーネントにデータを渡してみましょう。
TypeScriptで書くからには、コンポーネントのプロパティもきちんと型付けを行います。
[スターターコード](https://codepen.io/tyonekubo/pen/jOEGxqd?editors=0010)をブラウザの新しいタブで開いて、以下のコードを追加しましょう（コードをコピー・ペーストしないで、手でタイプすることをお勧めします）。

```react
type FeeClassification = {
  name: string;
  description: string;
  unitPrice: number;
  numOfPeople: number;
  totalPrice: number;
}

type DetailProps = {
  classification: FeeClassification;
}
```

`FeeClassification`はアプリケーション内で共通的に使用する型の定義です。その下の`DetailProps`が実際に`Detail`コンポーネントが利用するpropsの型定義となります。

次に、`Detail`コンポーネントのコードを修正します。
`React.Component`はpropsの型とstateの型を型パラメータとして受け取るので、先ほど作成した`DetailProps`をpropsの型に指定します。stateに関しては後で説明しますので今は気にしないでください。

そして、`div`要素のコンテンツや`select`要素の値にプロパティをバインドします。親のコンポーネントから渡されたプロパティセットは`this.props`で参照することができます。JSXの構文中において`{ }`で囲まれた範囲はTypeScriptのコードをして扱われます。

```react
class Detail extends React.Component<DetailProps, {}> {
  render() {
    return (
      <div >
        <div className="classification-name">{this.props.classification.name}</div>
        <div className="description">{this.props.classification.description}</div>
        <div className="unit-price">{this.props.classification.unitPrice}円</div>
        <div className="num-people">
          <select value={this.props.classification.numOfPeople}>
            <option value="0">0</option>
            <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option>
            <option value="4">4</option>
          </select>
          <span>名</span>
        </div>
      </div>
    );
  }
}
```

親コンポーネントにあたる`AdmissionFeeCalculator`では、`Detail`コンポーネントにプロパティを渡す必要があります。`DetailProps`型の各プロパティが、即ちJSXの`Detail`要素の属性となりますので、以下のようにコードを修正してプロパティを渡してください。

```typescript
class AdmissionFeeCalculator extends React.Component {
  private detail: DetailProps = {
    classification: {
	  name: "大人",
      description: "",
      unitPrice: 1000,
      numOfPeople: 0,
      totalPrice: 0
	}
  };
	
  render() {
    return (
      <>
        <Detail classification={this.detail.classification} />
        <Summary />
      </>
    );
  }
}
```

以下のように画面が表示されるようになったはずです。

<img width="460" alt="react-ex-03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176589/aa153629-2708-f475-3c73-4f9de4b4fbe3.png">

正しく表示されない場合は、CodePenのConsoleに何かエラーが出ていないか確認しましょう。
この時点でのコードは[このように](https://codepen.io/tyonekubo/pen/Baywxxv?editors=0010)なっているはずです。

# コンポーネントを繰り返し表示する

次に`Detail`コンポーネントを複数個繰り返して表示するようにしましょう。
まずは繰り返し表示するデータを、`AdmissionFeeCalculator`コンポーネント内に準備します。

```react
class AdmissionFeeCalculator extends React.Component {
  private details: DetailProps[] = [
    {
      classification: {
        name: "大人",
        description: "",
        unitPrice: 1000,
        numOfPeople: 0,
        totalPrice: 0
      }
    },
    {
      classification: {
        name: "学生",
        description: "中学生・高校生",
        unitPrice: 700,
        numOfPeople: 0,
        totalPrice: 0,
      }
    },
    {
      classification: {
        name: "子ども",
        description: "小学生",
        unitPrice: 300,
        numOfPeople: 0,
        totalPrice: 0,
      }
    },
    {
      classification: {
        name: "幼児",
        description: "未就学",
        unitPrice: 0,
        numOfPeople: 0,
        totalPrice: 0,
      }
    },
  ];

```

`render`メソッドでは、まずは複数行の明細を表すJSX（の配列）を作成します。コードサンプルでは配列の`map`メソッドを使ってJSX要素の配列へ変換しています。定数`detailsJsx`の実際の型は`JSX.Element[]`となります。

ここで注意すべき点は、繰り返し表示するコンポーネントに対しては`key`属性を付与する必要があるということです。これは、繰り返し表示する対象のモデルに変更が加えられたときに、変更が発生したアイテムを特定するために必要な識別子です。
`key`属性には、アイテムを特定可能な任意の値をセットします。今回のサンプルはアイテムの入れ替りは発生しないので、単純にインデックスをキーとしています。

あとは本体のJSX内に明細のJSX配列を展開（`{detailJsx}`の部分）するだけです。

```react

  render() {
    const detailsJsx = this.details.map((fc, idx) => {
      return (
        <Detail key={idx.toString()} classification={fc.classification} />
      );
    });

    return (
      <>
        {detailsJsx}
        <Summary />
      </>
    );
  }
```

画面表示は以下のように変わったはずです。

<img width="455" alt="react-ex-04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176589/af945046-241b-0b62-5e9b-47ea16495adb.png">

この時点でのコードは[このように](https://codepen.io/tyonekubo/pen/povWKmW?editors=0010)なっているはずです。

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

ここまでの実装で画面に4行の明細を表示できるようになりましたが、画面から人数のセレクトの値を変更しても画面上の表示は何も変わりません。変更を反映するためには、変更イベントを拾ってモデルを更新する必要があります。

ここで重要なのは、コンポーネントのプロパティは親コンポーネントから渡されますが、それは読取り専用であるということです。
何らかのイベントによって変更が発生しうる状態は、コンポーネント自身の`state`として管理します。

まずは`props`と同様、`state`に対応する型定義を行います。

```react
type DetailState = {
  numOfPeople: number;
}
```

定義した型を`React.Component`を継承する際の二つ目の型パラメータに指定し、コンストラクタでは`state`を初期化します。

```typescript
class Detail extends React.Component<DetailProps, DetailState> {

  constructor(props: DetailProps) {
    super(props);
    this.state = {
      numOfPeople: props.classification.numOfPeople,
    }
  }
```

コンストラクタに続いて、`Change`イベントのハンドラを作成し、変更された値で`state`を更新します。

この時、`state`オブジェクトを直接更新してはいけません。新しい`state`オブジェクトを作成して、`setState`メソッドを呼び出して更新をするのがReactにおける作法となります。

```react
  onNumOfPeopleChange(e: React.ChangeEvent<HTMLSelectElement>): void {
    const num: number = Number(e.target.value);
    this.setState({
      numOfPeople: num,
    });
  }
```

`render`メソッドのJSXでは、`select`要素にイベントハンドラを記述します。以下のサンプルコードのように、アロー関数を使った記述がよいでしょう。

```typescript
          <select value={this.state.numOfPeople}
            onChange={e => this.onNumOfPeopleChange(e)}>

```

これで、セレクトの選択値変更が実際に画面上に反映されるようになりました。
この時点でのコードは[このように](https://codepen.io/tyonekubo/pen/yLyzqOK?editors=0010)なっているはずです。

# stateを親コンポーネントへリフトアップする
## コンポーネント間の連携の流れ
次に、`Detail`コンポーネントの人数変更に呼応して`Summary`コンポーネントの合計人数と合計額を変更するように修正しましょう。
コンポーネントの親子構造を模式的に記述すると以下のようになっていました。

```react
<AdmissionFeeCalculator>
  <Detail />
  <Detail />
  <Detail />
  <Detail />
  <Summary />
</AdmissionFeeCalculator>
```

さて、Reactコンポーネントが参照や更新をできるデータは以下の二種類です。

* 親コンポーネントから渡されたプロパティ(props）　〜参照のみ可
* 自分自身の管理するステート(state)　〜参照・更新とも可

`Summary`コンポーネントは、兄弟関係にあたる`Detail`の内部状態を見ることはできません。
ではどうすればよいかというと、以下のような流れでコンポーネント間の連携を行うことになります。

1. `Detail`コンポーネントのイベントハンドラは、`props`経由で親コンポーネントに変更を通知する
2. `AdmissionFeeCalculator`コンポーネントは、自身の`state`を更新する
3. `AdmissionFeeCalculator`コンポーネントの`state`の一部は、`Summary`コンポーネントの`props`としてバインドされるため、変更が`Summary`コンポーネントへ伝搬して再描画される

順番に実装していきましょう。

## AdmissionFeeCalculatorコンポーネントの修正

まず、`Detail`コンポーネントで管理していた状態を、親にあたる`AdmissionFeeCalculator`コンポーネントに引き上げて管理するようにします（リフトアップ）。

```typescript
type AdmissionFeeCalculatorState = {
  feeClassifications: FeeClassification[];
}

class AdmissionFeeCalculator extends React.Component<{}, AdmissionFeeCalculatorState> {
```

コンストラクタで`state`の初期値を設定します。（もともと明細データを格納していたインスタンス変数は削除します）。

```react
  constructor(props: {}) {
    super(props);
    const adults: FeeClassification = {
      name: "大人",
      description: "",
      unitPrice: 1000,
      numOfPeople: 0,
      totalPrice: 0,
    };
    const students: FeeClassification = {
      name: "学生",
      description: "中学生・高校生",
      unitPrice: 700,
      numOfPeople: 0,
      totalPrice: 0,
    };
    const children: FeeClassification = {
      name: "子ども",
      description: "小学生",
      unitPrice: 300,
      numOfPeople: 0,
      totalPrice: 0,
    };
    const infants: FeeClassification = {
      name: "幼児",
      description: "未就学",
      unitPrice: 0,
      numOfPeople: 0,
      totalPrice: 0,
    };
    this.state = { feeClassifications: [adults, students, children, infants] };
  }
```

続いて、`Detail`コンポーネントで発生した`Change`イベントを処理するハンドラメソッドを`AdmissionFeeCalculator`コンポーネントに作成します。
ここでも、`state`の直接更新は禁じられているので、新しい`state`オブジェクトを作成して`setState`メソッドを呼び出す手順となります。

```react
  handleNumOfPeopleChange(idx: number, num: number) {
    const currentFC = this.state.feeClassifications[idx];
    const newTotalPrice = currentFC.unitPrice * num;
    // 人数と合計額以外は既存の値をコピー
    const newFC: FeeClassification =
      Object.assign({}, currentFC, { numOfPeople: num, totalPrice: newTotalPrice });
    // 新たな配列を生成
    const feeClassifications = this.state.feeClassifications.slice();
    feeClassifications[idx] = newFC;

    // stateの更新
    this.setState({ feeClassifications: feeClassifications });
  }
```

次に`render`メソッドを修正します。

* `Detail`コンポーネントの`onNumOfPeopeChange`属性には、先ほど定義した`handleNumOfPeopleChange`メソッドを呼び出すアロー関数を記述します。
* `state`で管理している情報から合計人数と合計額を計算し、`Summary`コンポーネントの属性に渡します。
* これらのコンポーネントの属性は、このあと作成するものです。

```react
  render() {
    const details = this.state.feeClassifications.map((fc, idx) => {
      return (
        <Detail key={idx.toString()} classification={fc}
          onNumOfPeopleChange={n => this.handleNumOfPeopleChange(idx, n)} />
      );
    });
    const numOfPeople = this.state.feeClassifications
      .map(fc => fc.numOfPeople).reduce((p, c) => p + c);
    const totalAmount = this.state.feeClassifications
      .map(fc => fc.totalPrice).reduce((p, c) => p + c);

    return (
      <>
        {details}
        <Summary numOfPeople={numOfPeople} totalAmount={totalAmount} />
      </>
    );
  }
```

## Detailコンポーネントの修正
`DetailProps`に、親コンポーネントに変更を通知するための関数`onNumOfPeopleChange`を定義します。関数の実体は親コンポーネントから渡されることになります。
`state`は親コンポーネントに引き上げて不要になったので、`DetailState`は削除します。

```react
type DetailProps = {
  classification: FeeClassification;
  onNumOfPeopleChange: (num: number) => void;
}
```

自身の`onNumOfPeopleChange`メソッドでは、`props`の`onNumOfPeopleChange`を呼び出すように修正します。

```react
class Detail extends React.Component<DetailProps, {}> {

  onNumOfPeopleChange(e: React.ChangeEvent<HTMLSelectElement>): void {
    const num: number = Number(e.target.value);
    this.props.onNumOfPeopleChange(num);
  }
```

`render`メソッド内のJSXの`select`要素を以下のように修正します。

```react
          <select value={this.props.classification.numOfPeople}
                  onChange={e => this.onNumOfPeopleChange(e)}>
```

## Summaryコンポーネントの修正
`Summary`コンポーネントの`props`の型定義を作成しましょう。

```react
type SummaryProps = {
  numOfPeople: number;
  totalAmount: number;
}
```

`SummaryProps`を`React.Component`継承時の型パラメータに指定するとともに、`props`のデータを参照するようにJSXを修正します。

```react
class Summary extends React.Component<SummaryProps, {}> {
  render() {
    return (
    <div>
      <div className="party">
        <input type="text" className="party" value={this.props.numOfPeople} />
        <span>名様</span>
      </div>
      <div className="total-amount">
        <span>合計</span>
        <input type="text" className="total-amount" value={this.props.totalAmount} />
        <span>円</span>
      </div>
    </div>
    );
  }
}
```

これで、明細の人数を変更すると再計算が行われて、結果がサマリ欄に反映されるようになりました。実際に画面から動作を確認してみてください。
この時点でのソースコードは[このように](https://codepen.io/tyonekubo/pen/RwNLdLa?editors=0010)なっているはずです。

# 関数コンポーネントに置き換える
最後にリファクタリングを行います。`Detail`および`Summary`コンポーネントは自分自身では何の状態も管理していません（言い換えれば、`state`を管理していません）。
このような場合は、`React.Component`を継承したクラスを作成するのではなく、関数としてコンポーネントを定義することができます。

`Detail`コンポーネントは以下のように修正します。

* 型は`React.FC<propsの型>`とします
* propsを受け取ってJSXを返却するアロー関数として定義します
* これまで`this.props`を参照していた箇所は（引数の）`props`を参照する形になりますので注意してください

```react
const Detail: React.FC<DetailProps> = props => {
  const onNumOfPeopleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const num: number = Number(e.target.value);
    props.onNumOfPeopleChange(num);
  }

  return (
    <div >
      <div className="classification-name">{props.classification.name}</div>
      <div className="description">{props.classification.description}</div>
      <div className="unit-price">{props.classification.unitPrice}円</div>
      <div className="num-people">
        <select value={props.classification.numOfPeople}
                onChange={e => onNumOfPeopleChange(e)}>
          <option value="0">0</option>
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
          <option value="4">4</option>
        </select>
        <span>名</span>
      </div>
    </div>
  );
}
```

同様に`Summary`コンポーネントも関数コンポーネントに修正します。

```react
const Summary: React.FC<SummaryProps> = props => {
  return (
    <div>
      <div className="party">
        <input type="text" className="party" value={props.numOfPeople} />
        <span>名様</span>
      </div>
      <div className="total-amount">
        <span>合計</span>
        <input type="text" className="total-amount" value={props.totalAmount} />
        <span>円</span>
      </div>
    </div>
  );
}
```

これでチュートリアルはすべて完了です！：[最終結果](https://codepen.io/tyonekubo/pen/KKwXRpJ?editors=0010)

# 次に学ぶこと
React 16.8.0でフックが導入されて（React Hooks）以降は、あらゆるコンポーネントを関数型コンポーネントで実装するやり方が主流となっています。クラスコンポーネントも引き続きサポートされ続ける予定なので、本チュートリアルで学んだ内容はこの先も有効ですが、今後はWebや書籍のサンプルもHooksを用いたものが多くなると予想されます。

[公式のHooksのドキュメント](https://ja.reactjs.org/docs/hooks-intro.html)や、本チュートリアルの続編にあたる[TypeScriptでReact Hooksに入門するチュートリアル](https://qiita.com/yonetty/items/ad19bddc26806bb49cef)でHooksにもチャレンジしてみてください。

# (Appendix) ローカル開発環境のセットアップ
## 前提条件
* `node`と`npm`がインストール済みであること

## create-react-appでアプリケーションを作成する

```bash
$ npx create-react-app ts-tutorial --template typescript
```

※`--typescript`は非推奨となり、`--template typescript`となりました（2020/2/4修正済)

## アプリケーション起動

```bash
$ cd ts-tutorial
$ yarn start
```

`http://localhost:3000/` でアプリケーションにアクセスできるようになります。（Ctrl+Cで終了）

## CSSの修正
`src/App.css` を以下の内容で置き換えてください。

```css
div.main {
  margin: 10px;
}

div.classification-name {
  height: 30px;
  width: 100px;
  float: left;
  clear: both;
}

div.description {
  height: 30px;
  width: 150px;
  float: left;
}

div.unit-price {
  height: 30px;
  width: 100px;
  float: left;
}

div.num-people {
  height: 30px;
  width: 100px;
  float: left;
}

div.party {
  margin-top: 10px;
  height: 30px;
  width: 150px;
  background-color: azure;
  float: left;
  clear: both;
}

div.total-amount {
  margin-top: 10px;
  height: 30px;
  width: 200px;
  background-color: azure;
  float: left;
}

input.party {
  width: 50px;
  text-align: right;
  margin-left: 5px;
  margin-right: 5px;
}

input.total-amount {
  width: 80px;
  text-align: right;
  margin-left: 5px;
  margin-right: 5px;
}
```

## App.tsxの修正
`src/App.tsx`を以下の内容で置き換えます。
これで、チュートリアル本編のスターターコードと同等の状態になります。

```react
import React from 'react';
import './App.css';

class Detail extends React.Component {
  render() {
    return (
      <div >
        <div className="classification-name">名前</div>
        <div className="description">説明</div>
        <div className="unit-price">0円</div>
        <div className="num-people">
          <select value="0">
            <option value="0">0</option>
            <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option>
            <option value="4">4</option>
          </select>
          <span>名</span>
        </div>
      </div>
    );
  }
}

class Summary extends React.Component {
  render() {
    return (
      <div>
        <div className="party">
          <input type="text" className="party" value="0" />
          <span>名様</span>
        </div>
        <div className="total-amount">
          <span>合計</span>
          <input type="text" className="total-amount" value="0" />
          <span>円</span>
        </div>
      </div>
    );
  }
}

class AdmissionFeeCalculator extends React.Component {
  render() {
    return (
      <>
        <Detail />
        <Summary />
      </>
    );
  }
}

const App: React.FC = () => {
  return (
    <div className="main">
      <AdmissionFeeCalculator />
    </div>
  );
}

export default App;
```

## ソースコード
ローカル開発版のチュートリアルの最終形は[GitHubで公開](https://github.com/yonetty/admission-fee-react)してあります。