C#エンジニアに贈るReact入門 

この記事は [React #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/react2) 19日目の記事です。


# はじめに

本職は、C#でWindows Formを使ったレガシーなアプリケーションを作っています。

ただ、レガシーなアプリを作っていると、Webフロントエンドは、進化が早くて、活発で、
なんかキラキラしている感じがして、うらやましくなってきます。

Reactは、趣味だけなのでまだまだですが、そんなC#エンジニア(以降、C#er)から見たReact入門を解説したいと思います。

同僚がReactを勉強したいと言い出したので、そのための資料でもあります。

React の Advent Calendar ということで他の方がReactのTipsを公開している中、
一人だけ空気を読まなくて申し訳ないです・・・。

なお、Webフロントエンド界隈は進化や世代交代が早いので、
この記事の賞味期限もせいぜい半年～1年くらいだと思っています。

(2020-09-02 Modified)
予想通りライブラリやツールのバージョンアップによっていろいろ変更なようです。
この記事は自分でも使っていて不便なので最低限問題ないように修正しました。
(もしかしたら抜けがあるかも？)

(2020-10-04 Modified)
上の対応ではだめだったので、さらに追記しました。

(2020-12-31 Modified)
なんかAdvent Calendar 2020の影響でPVが増えたのかストックされる方が増えたので
いろいろ動かないところを最低限アップデートしました。
* MobX ver.6.0.0 以降に対応
* .envファイルは不要になった
* Visual Studio Codeでnode_modules以下のTypeScriptを使用する設定

## 要約

* C#エンジニアがReactを始めるときは、Typescript、MobX、Material-UI、Class Componentを使うといいよ。
* 解説をつけながらハンズオン的な感じでブログっぽいページを作るよ

## 想定読者

* WindowsでC#を使ってWindows Formなアプリを作ってきた方
* および、Webアプリは、CGIとかAjaxあたりで止まっている方


# React基礎知識

## そもそもReactって？

Webアプリのフロントエンドを構築するためのJavascriptライブラリです。
最近の流行として、htmlのページ遷移無しで、1ページ内で完結させようという流行(SPA、Single Page Application)があり、ReactはSPAを実現するためのフレームワークの1つです。
主な役割はhtmlのレンダリングであり、C#でいうと、WPFにおけるXAMLや、WindowsFormsの*.Designer.csに相当します。

## Reactって何がうれしいの？

レガシーなWebサイトの場合、htmlでコンテンツを管理して、 見た目はCSS、何か動かすならJavascriptという感じだったと思います。

ただ、htmlで使えるのは決まったタグだけで、 部品化ができず、凝ったことをしようとすると作りこみが必要なわりに、それを他のページに展開するにはコピペが必要になってしまいます。

Reactを使うと、独自のHTMLタグを増やすことができるようになります。
さらに、単なるhtmlの部品ではなく、Javascriptもセットになっているので、
部品化によってリッチなUIを簡単に作ることができるようになります。

C#でいうなら、既定のButtonやLabelしか使えない状況から、UserControlが使えるようになったものです。

公開されている部品を組み合わせてもいいですし、もちろん自分で部品を作ることもできます。

例えば、次のようなブログのトップページをイメージしてもらうと、

![Sample-Blog-Top.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/ded42112-51ab-4b2e-9dfe-36c6a87ad355.jpeg)


1つの記事のサマリーが`AbstractContent`という部品になっていれば、 それを並べればTopページが作れます。

```react
<div className="TopPage">
	<AbstractContent title={title1} .... />
	<AbstractContent title={title2} .... />
	<AbstractContent title={title3} .... />
</div>
```

さらに`AbstractContent`も何かの部品を組み合わせているので 次のようなツリー構造になります。

![Blog-Component-Tree.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/7e4c21f8-5254-6325-b326-9376cc75c9e5.jpeg)


部品の例としては、material-uiのサイトを眺めるとこの部品化がイメージしやすいと思います。 

* https://material-ui.com/components/text-fields/

## Reactで部品はどうやって作るのか？

Reactの部品はこんな感じで作ります。

```react:Reactコンポーネントの例
import React from "react";

interface BlogPartsProps {
  id: number;
  title: string;
  date: string;
  abstract: string;
}

class BlogParts extends React.Component<BlogPartsProps > {
  public render(): JSX.Element {
    return (
      <div>
      	<a href={`/contents/${this.props.id}`}><p>{this.props.title} {this.props.date}</p></a>
      	<p>{this.props.abstract}</p>
      </div>
    );
  }
}

export default BlogParts;
```

細かい説明は、他サイトを参考にしてもらったらよいと思いますが、
ポイントは次の2つです。

* render関数では、JavascriptとHtmlが混ざった書き方をする。C#でいうと、RazorやT4テンプレートみたいなもの。
* BlogPartsProps interfaceに定義されているフィールドが、外から渡されるパラメータ

BlogPartsPropsうんぬんは、C#のWindows Formで例えると、こんな感じで、外からセットされるプロパティに相当します。

```csharp:C#でのPropsの例
class BlogParts : UserControl
{
  public int id {get;set;}
  public string title {get;set;}
  public string date {get;set;}
  public string abstractContent {get;set;}
  
  private void BlogParts_Load(object sender, EventArgs e)
  {
      _titleLabel.Text = this.Title;
      ・・・
  }
}
```


なお、Reactは、classで部品を作るパターン(Class Component)と、関数で部品を作るパターン(Functional Component)がありますが、C#erとしては、前者のほうがイメージしやすいです。
ただし、今の流行や推奨は後者のFunctional Componentです。React Hooksとかもこちらのようです。

今回は、C#er向けということでClass Componentでいきます。

## Typescript

Typescriptは、Javascriptに型を導入した言語です。
ビルド(トランスクリプトというらしい)するとjavascriptになります。
ビルド結果のjavascriptは人が読むことは想定されていないようなので
ビルドするとバイナリになるC#に近い使用感です。
生のJavascriptは型がなく、なんとなくで実装できてしまいますが、
C#をやっている身からすると型が無いのが恐ろしいです。

ということで、Typescript、おすすめです。

言語的にはC#に近い言語っぽいけど、Javascriptの制約を引きずっているのと、独自進化しているようで、
知らない仕様がけっこうあります。


簡単なクラスを例にするとこんな感じです。

```typescript:Typescriptの例
export class AppStore
{
	_id:number|undefined;  // 型はメンバーの後ろ、また、型に"|"を使ってORを表現できる。
	
	// メソッドはラムダ式を代入したメンバー。C#のメソッドっぽくも書けるけどこっちのほうがいいみたい
	public changeId = (id:number):void => {  
		this._id = id;
	}
	public getId = ():number => {
		if(this._id !== undefined)  //undefinedでないことをチェックしないとビルドが通らない
		{
			return this._id;
		}
		return 0;
	}
}

```


## Windowsアプリでも問題になるけど内部状態はどう管理したらいいの？

React界隈でも問題になったようで、[Flax](https://facebook.github.io/flux/)というアーキテクチャが生み出されています。

![flux.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/38209bb8-37ec-10e7-3d2e-842c8320cd87.jpeg)


参考: https://facebook.github.io/flux/


簡単に解説すると、(1)内部状態(Store)は1か所に集約する、
(2)GoFのCommandパターンで状態を更新する(ActionとDispatcher)、
(3)Viewは内部状態をただ表現するだけ、のようです。
この発想自体は、Windowsアプリケーションでも参考になりそうですね。

ただ、理念は良さそうですが、Flax実装の一つで一番人気のReduxは肌に合わなかったです。
単にReduxの実装がよくないだけかもしれませんし、
Commandパターンにありがちな、無駄に実装量が増えるあたりかもしれません。

代替策を調べた結果、もう少し簡易的な状態管理ライブラリである、MobXが良さそうに思えました。
MobXは、上の(1)～(3)の中では、(2)はあきらめて、
DIとGoFのObserverパターンを追加した感じです。
WPFにMVVMパターンがありますが、それに似ています。

すなわち、いろいろなViewModelは1か所で管理されて、Viewの定義に従ってDIされる、
ViewModelで状態が変わるとView側(React)に通知が来て、再描画される、という感じです。


![MobX_Abstract.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/7d91a594-1634-8d85-97ac-7c1ee4abc42a.jpeg)



C#でMobXに似たような実装を書くとこんな感じ。

```csharp:C#でのMobXっぽい実装イメージ
public class App: UserControl
{
	ViewModel _viewModel;
	
	[Inject("viewModel")]  //この属性を目印にDIされる
	public void SetViewModel(ViewModel viewModel)
	{
		_viewModel = viewModel;
		_viewModel.PropertiesUpdated += _viewModel_PropertiesUpdated;
	}
	
	_textBox1_TextChanged()
	{
		_viewModel.Text = _textBox1.Text;  //コントロールの変更はViewModelに伝える
	}
	
	_viewModel_PropertiesUpdated()  // ViewModelの変更から画面を更新する
	{
		// 描画更新
	}
}

public class ViewModel
{
	private string _text;
	public string Text {
		get { return _text; }
		set { _text = value; PropertiesUpdated?.Invoke();}  //プロパティの変更を通知する
	}
	public event Action PropertiesUpdated;
}
```

参考: https://mobx.js.org/README.html

# Reactをとにかく使ってみたい

さて、基礎知識はここまでとして、実際に手を動かしてみましょう。
思ったより長くなったので、興味ない方は、以降は読み飛ばして、まとめに行ってもらってもいいです。

## 事前に必要なもの

環境は、C#er前提なのでWindowsです(ほかの環境でも動きそうですが確認していません)。
また、次のアプリをインストールしておく必要があります。

* [Node.js](https://nodejs.org/en/)
* [Google Chrome](https://www.google.com/intl/ja_jp/chrome/)
   * [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-
tools/fmkadmapgofadopljbjfkapdkoienihi?hl=ja)( Reactの開発にあると便利ですが、今回は使いません)
* [Visual Studio Code](https://azure.microsoft.com/ja-jp/products/visual-studio-code/)
   * [ESLint Extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
   * [Prettier - Code formatter Extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)


なお、React本体やTypescriptコンパイラは開発用フォルダにダウンロードされますので 上記以外ではOSを汚しません。(・・・はず)

## 開発環境

まずは開発環境を作ります。 次のコマンドで、`create-react-app`というReact開発環境のテンプレートを生成するツールをダウンロードします。なお、npmはC#でいうところのNugetです。

```powershell:commandline
cd (適当な空フォルダー)
npm install create-react-app
```

ダウンロードしたモジュールはnode_modulesフォルダに入っており、実行モジュールは`node_modules/.bin`にあります。
それを使って、開発環境を生成します。
第一引数はフォルダ名(およびプロジェクト名)を、第二引数の`--typescript`はJavascriptではなくTypescriptを使うことを指定しています。

```powershell:commandline
node_modules\.bin\create-react-app test --typescript
```

出来上がった開発環境のフォルダに移動して、実行してみましょう。

```shell:commandline
cd test
npm start
```

<<2020-12-31 Deleted>>
この.envファイルは、最新のReactで不要になったようです。
一応、古いバージョンを使うこともあるので、記事はこのまま残しておきます。

<<2020-10-04 Added>>
`npm start` で開発用サーバーとブラウザが立ち上がるはずですが・・・
次のようなエラーが出ることがあります。
eslintのバージョンが高すぎると出るようですが、
reactにeslintが含まれているのでエラーになってしまいます。

```commandline
> npm start
There might be a problem with the project dependency tree.
It is likely not a bug in Create React App, but something you need to fix locally.

The react-scripts package provided by Create React App requires a dependency:

  "eslint": "^6.6.0"

Don't try to install it manually: your package manager does it automatically.
However, a different version of eslint was detected higher up in the tree:

  c:\work\trials-create-react-app\node_modules\eslint (version: 7.7.0)

Manually installing incompatible versions is known to cause hard-to-debug issues.

If you would prefer to ignore this check, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project.
That will permanently disable this message but you might encounter other issues.
```

対処方法は、エラー出力に書いてある通りで、一度eslintを消すのが正解ですが、
ここでは面倒なので、もう一つの方法であるエラー無視の方向にします。

次のようなファイルを作ります。

```:.env
SKIP_PREFLIGHT_CHECK=true
```

これで、 `npm start` でエラーが出なくなります。

<<2020-10-04 Added終わり>>
<<2020-12-31 Deleted終わり>>

<<2020-12-31 Added>>
Visual Studio CodeでApp.tsxを開いたときに次のようなエラーになることがあります。

![VSCode-Error-JSXUnlessJsxFlag.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/3f04c901-797f-62fa-593d-b81d527c022d.jpeg)


> Cannot use JSX unless the '--jsx' flag is provided.

エラーメッセージだけだと意味不明ですが、
Visual Studio Codeが使用するTypescriptのバージョンが古いと発生するようです。
Visual Studio CodeのステータスバーにTypeScriptのバージョンが表示されていますので、
それをクリックし、[Select TypeScript Version] → [Use Workspace Version]で
node_modules以下のTypeScriptを使用するように設定できます。

![VSCode-UseTypescriptVersion.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/6edad286-d7b2-2612-8d77-43ba82f24c7d.jpeg)

上記の設定をすると、settings.jsonに以下のように記録されますので、直接settings.jsonを編集しても良いようです。

```json:.vscode\settings.json
{
  "typescript.tsdk": "node_modules\\typescript\\lib",
}
```

<<2020-12-31 Added終わり>>


出来上がった開発環境のフォルダに移動して、実行してみましょう。

```shell:commandline
cd test
npm start
```


初期の画面が表示されます。 `npm start` は、Visual Studioのデバッグの開始のようなもので、
自動的にhttpサーバーが起動し、ソースがビルドされて、Webページが表示されます。

![React-init-screen.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/71dbfa6a-e0d4-ad00-327f-3aca607d6405.jpeg)


この状態で、ソースコードを一部変更してみます。
Visual Studio Code等のテキストエディタで`src/App.tsx`を編集します。


```diff:src/App.tsx
- Edit <code>src/App.tsx</code> and save to reload.
+ Hello World
```

ソースコードを変更すると自動でビルドされ、Webページも更新されます。

![React-init-screen-Helloworld.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/97450a2b-69ef-c894-2ebd-e1fef19c0f8c.jpeg)


終了は、コマンドプロンプトでCtrl+Cです。

開発環境のファイル構成はこんな感じです。
よく使うファイルは説明を付けました。

```
├ build         ・・・ビルド結果が入る。C#でいうとbin/Release
├ node_modules  ・・・ダウンロードしたパッケージがあります。C#でいうとpackages
├ public        ・・・この中身がビルド時にコピーされる。index.htmlとか入っている
├ src           ・・・ソースコードフォルダ
│  ├ App.css
│  ├ App.test.tsx ・・・App.tsxのユニットテスト。
│  ├ App.tsx      ・・・Web画面。ここをメインで編集します。C#でいうとForm1.Designer.cs
│  ├ index.css
│  ├ index.tsx    ・・・エントリーポイントです。C#でいうとProgram.csのmain関数
│  ├ logo.svg
│  ├ react-app-env.d.ts
│  ├ serviceWorker.ts
│  └ setupTests.ts
├ .env　　　　　　　　・・・環境変数の定義 (2020-10-04 Added)
├ .gitignore
├ package.json      ・・・このプロジェクトの情報が記載されている。C#でいうと*.csproj
├ package-lock.json ・・・依存パッケージの情報。package.jsonにもあるけど、こっちがマスター？
├ README.md
└ tsconfig.json     ・・・Typescriptの設定。
```

## 開発環境を作りこむ

このままでも開発可能ですが、コードチェックの設定を行って、楽をしましょう。
C#でいうと、Visual Studioのコード分析か、Resharperですね。

必要なパッケージをダウンロードします。

(2020-09-02 Modified)
reactをバージョンアップしたら、このやり方だとエラーが出るようになりました。eslintはすでに導入されているため追加でインストールしてはいけないようです。
参考: https://qiita.com/arakappa/items/9781556a8a67b6779a0e

```shell:commandline
npm install --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-prettier prettier

↓は古いやり方
npm install --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier eslint-plugin-prettier prettier
```


コードチェックのルール用ファイルを作成します。


```json:.eslintrc.json
{
    "extends": [
      "react-app",
      "eslint:recommended",
      "plugin:@typescript-eslint/recommended",
      "plugin:@typescript-eslint/eslint-recommended",
      "plugin:prettier/recommended",
      "prettier/@typescript-eslint"
    ],
    "plugins": [
      "@typescript-eslint"
    ],
    "parser": "@typescript-eslint/parser",
    "env": { "browser": true, "node": true, "es6": true },
    "parserOptions": {
      "sourceType": "module"
    },
    "rules": {
      
    }
  }
```

次のコマンドでソースコードのコードチェックが行われます。

```shell:commandline
node_modules/.bin/eslint src --ext .js,.jsx,.ts,.tsx
```


```:コードチェック結果
(WorkDir)\src\App.test.tsx
  1:19  error  Replace `'react'` with `"react"`                                        prettier/prettier
  2:24  error  Replace `'@testing-library/react'` with `"@testing-library/react"`      prettier/prettier
  3:17  error  Replace `'./App'` with `"./App"`                                        prettier/prettier

・・・

✖ 39 problems (33 errors, 6 warnings)
  33 errors and 0 warnings potentially fixable with the `--fix` option.

```

デフォルト状態では、大量にエラーが表示されます。

次に、Visual Studio Codeでの警告表示と保存時の自動Fixを設定します。
Visual Studio Codeの設定ファイルに以下を追加します。ファイルやフォルダがなければ新規作成してください。

(2020-09-02 Modified)
Visual Studio Codeをバージョンアップしたら動作しなくなりました。
新しいeslintプラグインの書き方に変更する必要があるようです。
参考: https://qiita.com/mysticatea/items/3f306470e8262e50bb70

```json:.vscode\settings.json
{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],
  "typescript.tsdk": "node_modules\\typescript\\lib",  // (2020-12-31 Added) Visual Studio Codeが使用するTypeScriptの設定
}
```

```json:.vscode\settings.json
(これは古いeslintプラグインでの書き方です。)
{
    "eslint.autoFixOnSave": true,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        {"language": "typescript", "autoFix": true },
        {"language": "typescriptreact", "autoFix": true }
      ]
}
```

これで、Visual Studio CodeでJavascriptやTypescriptのファイルを開いた場合に以下のように警告が表示され、
上書き保存時に自動修正できる箇所は修正されます。

![VSCode-Warning.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/e49d7a6f-0a72-42bc-47f1-9e6cc75dceb2.jpeg)


eslintの出力に記載があるように`--fix`をつけると自動Fixできますが、ファイル数も少ないので、
Visual Studio Codeで1つ1つファイルを開いては上書き保存していきます。

もう一度eslintを実行すると、自動解決できない警告だけになっています。 
ここでは、気にしない方向で無視します。
なお、`@typescript-eslint/explicit-function-return-type`を無視するように`eslintrc.json`に記載するか、`serviceWorker`の関数の戻り値をちゃんと宣言すれば、警告を消せます。

```shell:commandline
node_modules/.bin/eslint src --ext .js,.jsx,.ts,.tsx
```

```:コードチェック結果
(WorkDir)\src\serviceWorker.ts
   28:8   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   62:1   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   66:36  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   71:42  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
  106:1   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
  136:8   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type

✖ 6 problems (0 errors, 6 warnings)
```

毎回、上記のeslintコマンドを入れるのは面倒なので、mpn scriptにします。 (なおnode_modules/.bin のパスは、npmコマンドで実行すると解決されるので不要です)


```diff:package.json
-   "eject": "react-scripts eject"
+   "eject": "react-scripts eject", 
+   "lint": "eslint src --ext .js,.jsx,.ts,.tsx"
```

これで、`npm run lint`で実行できるようになります。

```shell:commandline
npm run lint
```

```:コードチェック結果
(WorkDir)\src\serviceWorker.ts
   28:8   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   62:1   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   66:36  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   71:42  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
  106:1   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
  136:8   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type

✖ 6 problems (0 errors, 6 warnings)
```

参考

* https://qiita.com/madono/items/a134e904e891c5cb1d20
* https://ginpen.com/2019/08/06/eslint-for-react-in-typescript/

## Class Componentにする

`App.tsx`のデフォルトは、Functional Componentです。
上で話した通り、C#er的にはClass Componentのほうがわかりやすいので、Class Componentに変更します。

といっても、わりと機械的な作業で、 `React.Component`を継承したclassを作って、
デフォルトで実装されていた`App`関数を`render`関数にします。

```react:src/App.tsx(変更前)
const App: React.FC = () => {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Hello World</p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
};
```

```react:src/App.tsx(変更後)
class App extends React.Component {
  public render = (): JSX.Element => {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>Hello World</p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  };
}
```

ポイントは以下です。

* `React.Component`を継承したclassを作ります。なお、Typescriptの継承は`extends`です。
* `reander`関数の戻り値はhtmlっぽい何かで、型的には`JSX.Element`になります。
  * `class` は `className` にする(javascriptの構文と名前が被るので)
  * Htmlの属性は文字列以外は{}で囲ってJavascriptまたはTypescriptで記載する
  * styleは `{{width:"50%"}}` のように`{}`で囲われたJsonで書く

`npm start`してみると、当然ですが、見た目は変わりません。

![React-init-screen-Helloworld.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/97450a2b-69ef-c894-2ebd-e1fef19c0f8c.jpeg)

参考: https://qiita.com/daikoncl/items/a3806d8a8bf35f086487

## Material-UIを導入する

Reactのデフォルトでは、標準のhtmlタグしか使えません。 デザインセンス無しでそこそこイケてるWeb画面を作るために、既存の部品を使います。
ここでは、上で紹介した、Material-UIです。

まずパッケージをインストールします。

```shell:commandline
npm install @material-ui/core @material-ui/icons --save
```

そして、material-uiを使ったUIに`App.tsx`をマルっと書き換えます。
`Card`を使って、ブログのサマリーページ的な感じにしました。
`Card`の使い方は、Material-UIの公式を参考に。
https://material-ui.com/components/cards/


```react:src/App.tsx
import React from "react";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import Typography from "@material-ui/core/Typography";
import "./App.css";

class App extends React.Component {
  render(): JSX.Element {
    return (
      <div className="App">
        <Card style={{ width: 300, margin: 2 }}>
          <CardHeader
            action={
              <IconButton aria-label="settings">
                <MoreVertIcon />
              </IconButton>
            }
            title="タイトル"
            subheader="2019-12-17"
          />
          <CardContent>
            <Typography color="textSecondary" gutterBottom>
              アブストラクトです。
            </Typography>
          </CardContent>
        </Card>
        <Card style={{ width: 300, margin: 2 }}>
          <CardHeader
            action={
              <IconButton aria-label="settings">
                <MoreVertIcon />
              </IconButton>
            }
            title="タイトルその2"
            subheader="2019-12-17"
          />
          <CardContent>
            <Typography color="textSecondary" gutterBottom>
              アブストラクトその2です。
            </Typography>
          </CardContent>
        </Card>
      </div>
    );
  }
}

export default App;
```

`npm start`してみると次のようになります。
いろいろスタイルは手抜きですが、影とかアイコンとかそれっぽくなりました。

![React-blog-top.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/4618c8a2-c0eb-27ce-3380-4a542136f9b0.jpeg)

参考: https://material-ui.com/

## データをサーバーから取得

現状、コンテンツは`App.tsx`に埋め込まれています。 
普通のサイトを考えるとJsonをサーバーから取得すると思います。

ざっくりJsonを想定するとこんな感じでしょうか。

```json
[
  {
    "id": 1
    "title": "タイトル",
    "date": "2019-12-17",
    "abstract": "アブストラクトです。"
  },
  {
    "id": 2
    "title": "タイトルその2",
    "date": "2019-12-17",
    "abstract": "アブストラクトその2です。"
  }
]
```

Webページの組み込みは、いったん後回しとして、 サーバーからJsonを取得して、このデータをWebページに渡すViewModelを作り、 単体テストで動作確認します。

まずは、サーバーがないのでテスト時にHTTPリクエストを代替するパッケージ`fetch-mock`を入れます。

```shell:commandline
npm install fetch-mock @types/fetch-mock node-fetch --save-dev
```

そして、ViewModelとユニットテスト用に2つのファイルを追加します。


```typescript:src/AppStore.ts　(ViewModelです)
export interface BlogSummary {
  id: number;
  title: string;
  date: string;
  abstract: string;
}

export class AppStore {
  data: BlogSummary[] = [];
  public fetchData = async (): Promise<void> => {
    const json = (await (
      await fetch("/api/summaries/")
    ).json()) as BlogSummary[];
    this.data = json;
  };
}
```



```typescript:src/AppStore.test.ts　(ユニットテストです)
import { AppStore } from "./AppStore";
import fetchMock from "fetch-mock";

const dummyData = [
  {
    id: 1,
    title: "タイトル",
    date: "2019-12-17",
    abstract: "アブストラクトです。"
  },
  {
    id: 2,
    title: "タイトルその2",
    date: "2019-12-17",
    abstract: "アブストラクトその2です。"
  }
];
fetchMock.get("/api/summaries/", dummyData);

test("fetchData test", () => {
  const target = new AppStore();
  target.fetchData().then(() => {
    expect(target.data).toEqual(dummyData);
  });
});
```

ついでに、上の変更ですでに壊れているので `src/App.test.tsx` を削除します。
(個人的にViewのテストは難しいわりにリターンが少ないので、あまりしません。
 Typescriptで型はチェックできていますし。)

ユニットテストを実行してみましょう。

```shell:commandline
npm run test
```

成功しました。良さそうです。

```:ユニットテスト結果
 PASS  src/AppStore.test.ts
  √ fetchData test (5ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.573s
Ran all test suites related to changed files.

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press q to quit watch mode.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press Enter to trigger a test run.
```

参考: http://www.wheresrhys.co.uk/fetch-mock/

## MobXを使ってViewModelとViewを結合する

ViewModel `AppStore` はルートである `Index.tsx` の `stores` で管理し、`App` にインジェクトします。


![Object-Diagram1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/be3073a2-e463-95c7-e6e2-93ccfb34c565.jpeg)



まずは必要なパッケージをインストールします。

```shell:commandline
npm install mobx mobx-react --save
```

そして、MobXはデコレーターを使用するのでTypescriptのオプションを変更します。

```diff:tsconfig.json
-    "jsx": "react"
+    "jsx": "react",
+    "experimentalDecorators": true
  },
```

index.tsxでストア(stores)を作り、 状態を管理する`<Provider></Provider>`で`App`を囲い、
`Provider`には`stores`を渡します。
これにより、`App`の要求に従って、`Provider`から必要なViewModelを渡されます。


```diff:src/index.tsx
- ReactDOM.render(<App />, document.getElementById("root"));
+ import { AppStore } from "./AppStore";
+ import { Provider } from "mobx-react";
+ 
+ const stores = {
+   appStore: new AppStore()
+ };
+ 
+ ReactDOM.render(
+   <Provider {...stores}>
+     <App />
+   </Provider>,
+   document.getElementById("root")
+ );
```

ストアの `appStore` を受け取るように `App` で宣言します。


```typescript:src/App.tsx(変更前)
class App extends React.Component {
```


```typescript:src/App.tsx(変更後)
import { AppStore } from "./AppStore";
import { inject, observer } from "mobx-react";

interface AppProps {
  appStore?: AppStore;
}

@inject("appStore")
@observer
class App extends React.Component<AppProps> {
```

ポイントは以下です。

* React部品で外からパラメータを受け取るにはPropsの仕組みを使います。
  * `React.Component`のジェネリック型に、受け取るパラメータを宣言したインターフェイス`AppProps`を指定します。
  * 今回は`stores`で管理されている`AppStore`を受け取るので、`AppStore`型の`appStore`を宣言します。
* `@inject`の引数および`AppProps`のプロパティには、受け取りたい`stores`内のプロパティ名`appStore`を指定します。

これで、`index.tsx`の`stores`内の`appStore`が`App`に渡されます。

なお、当然ですが、見た目は変わりません。


![React-blog-top.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/4618c8a2-c0eb-27ce-3380-4a542136f9b0.jpeg)

## ViewModelを使ってWebページを更新する

上までで、`App`にViewModelである`AppStore`が渡されたので、`AppStore`の状態を使ってWebページを更新するようにします。 `App.tsx`をマルっと書き換えます。


```react:src/App.tsx
@inject("appStore")
@observer
class App extends React.Component<AppProps> {
  // コンポーネントが構築される直前に呼ばれる
  public componentDidMount = (): void => {
    if (this.props.appStore === undefined) {
      return;
    }
    this.props.appStore.fetchData();
  }

  render(): JSX.Element {
    // appStoreはundefindedである場合があるので、その時は何も表示しない
    if (this.props.appStore === undefined) {
      return <div />;
    }

    // this.props.appStore.dataに従ってWebページを構築する
    return (
      <div className="App">
        {this.props.appStore.data.map(data => {
          return (
            <Card key={data.id} style={{ width: 300, margin: 2 }}>
              <CardHeader
                action={
                  <IconButton aria-label="settings">
                    <MoreVertIcon />
                  </IconButton>
                }
                title={data.title}
                subheader={data.date}
              />
              <CardContent>
                <Typography color="textSecondary" gutterBottom>
                  {data.abstract}
                </Typography>
              </CardContent>
            </Card>
          );
        })}
      </div>
    );
  }
}
```

ポイント

* `componentDidMount()` はこのコンポーネントが構築される前に呼ばれます。
  * C#でいうところの、`Form.Load`イベントです。
  * ここでは、サーバーからデータを取得するため、 `appStore.fetchData()` を呼び出します。
* `render()` では、 `this.props.appStore.data` に従ってWebページを構築します。
  * `{}`で囲うとTypescriptを記述でき、returnで`JSX.Element`を返します。

MobXは、プロパティが変更されたことを検知して、Viewを再描画させる機能を持っています。
これを使って、データが取得されたときにViewを更新します。
そのために `AppStore` のメソッドとプロパティに属性を付けます。

(2020-12-31 Add)
MobX ver.6.0.0以降では、属性を使用しない仕組みに変わったようです。
コンストラクタで `makeAutoObservable(this);` を呼ぶと
自動でメンバーに `@observable` を付けたことになるようです。
`@observable` は不要になったようですが、私はなんとなく毎回付けてしまっています。
参考: https://mobx.js.org/migrating-from-4-or-5.html#upgrading-classes-to-use-makeobservable


```diff:src/AppStore.ts
+ import { action, makeAutoObservable, observable } from "mobx";


 export class AppStore {
+  constructor() {
+    makeAutoObservable(this);
+  }
+  @observable
   data: BlogSummary[] = [];
+  @action
   public fetchData = async (): Promise<void> => {
 }
```

これでサーバーデータに応じた画面を作ることができましたが、
このままでは、サーバーは存在しないのでエラーになります。
ユニットテスト用の`fetch-mock`を流用してダミーデータを使うようにします。


```diff:src/index.tsx
+ import fetchMock from "fetch-mock";
+ 
+ const dummyData = [
+   {
+     id: 1,
+     title: "[サーバー]タイトル",
+     date: "2019-12-17",
+     abstract: "アブストラクトです。"
+   },
+   {
+     id: 2,
+     title: "[サーバー]タイトルその2",
+     date: "2019-12-17",
+     abstract: "アブストラクトその2です。"
+   }
+ ];
+ fetchMock.get("/api/summaries/", dummyData);
  
  const stores = {
    appStore: new AppStore()
  };
```

これで、 `npm start` すると以下のようになります。

![React-blog-top-json.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/6f1ec277-e671-9ae5-0d32-a60c28ca289a.jpeg)


`dummyData`をいろいろ変えてみるとWebページが変わると思います。

## 部品を作る

Reactの部品化の練習のためにTOPページのサマリー要素部分を部品化してみます。

![Object-Diagram2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/88e3ac5f-26a0-53f0-b0ee-02fe01ddd967.jpeg)


上と同じようにReactコンポーネントのclassを作って、
renderでは、App.tsxで部品化したい部分を返すようにします。
必要なパラメータはPropsで上から渡してもらいます。
なお、Storesで状態管理するのはページごとで、部品ではViewModelは使わないほうが良いようです。

```React:src/SummaryItem.tsx(新規作成)
import React from "react";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import Typography from "@material-ui/core/Typography";
import { Link } from "react-router-dom";

interface SummaryItemProps {
  title: string;
  date: string;
  abstract: string;
  url: string;
}

class SummaryItem extends React.Component<SummaryItemProps> {
  render(): JSX.Element {
    return (
      <Card style={{ width: 300, margin: 2 }}>
        <CardHeader
          action={
            <IconButton aria-label="settings">
              <MoreVertIcon />
            </IconButton>
          }
          title={<Link to={this.props.url}>{this.props.title}</Link>}
          subheader={this.props.date}
        />
        <CardContent>
          <Typography color="textSecondary" gutterBottom>
            {this.props.abstract}
          </Typography>
        </CardContent>
      </Card>
    );
  }
}

export default SummaryItem;
```

`App.tsx`のほうは、上で作った部品を使うように書き換えます。
なお、部品の参照は、`import`です。
また、不要になった`import`も消しておきましょう。

```diff:src/App.tsx
- import Card from "@material-ui/core/Card";
- import CardHeader from "@material-ui/core/CardHeader";
- import CardContent from "@material-ui/core/CardContent";
- import IconButton from "@material-ui/core/IconButton";
- import MoreVertIcon from "@material-ui/icons/MoreVert";
- import Typography from "@material-ui/core/Typography";
+ import SummaryItem from "./SummaryItem";


-            <Card key={data.id} style={{ width: 300, margin: 2 }}>
-              <CardHeader
-                action={
-                  <IconButton aria-label="settings">
-                    <MoreVertIcon />
-                  </IconButton>
-                }
-                title={data.title}
-                subheader={data.date}
-              />
-              <CardContent>
-                <Typography color="textSecondary" gutterBottom>
-                  {data.abstract}
-                </Typography>
-              </CardContent>
-            </Card>
+            <SummaryItem
+              key={data.id}
+              title={data.title}
+              date={data.date}
+              abstract={data.abstract}
+            />
```

相変わらず見た目は変わりませんが、
サマリー部分の部品化ができました。

![React-blog-top-json.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/6f1ec277-e671-9ae5-0d32-a60c28ca289a.jpeg)

なお、この部品は再利用しないので、部品化はあまり意味はありません。

## ページ遷移を作る

SPAといえども、普通のHtmlのようにページ遷移がしたいことが多いです。
概要←→詳細 で行き来することもありますし、1ページいろいろ押し込むと使いにくいですし。

ここでは、`React-Router`を使ってページ遷移を作ります。

まずページ遷移があるということはURLを設計しないといけません。
簡単ですが、以下のようにします。

* `http://～/` 　　　　　　　　→ トップページ(サマリーのページ)
* `http://～/contents/(Id)` 　→ 詳細ページ(Idは数字)


では作ってみましょう。

まず、必要なパッケージを取得します。

```shell:commandline
npm install react-router-dom @types/react-router-dom  --save
```

次にTOPページから詳細ページへのリンクを作ります。
React-Routerでのリンクは`<Link to="url">ラベル</Link>`を使いますので、
タイトル部分をリンクに置き換えます。
URLはPropsに追加して、親ページから渡してもらうことにします。

```diff:src/SummaryItem.tsx
  import Typography from "@material-ui/core/Typography";
+ import { Link } from "react-router-dom";
 
  interface SummaryItemProps {
    title: string;
    date: string;
    abstract: string;
+   url: string;
  }
 
             </IconButton>
           }
-          title={this.props.title}
+          title={<Link to={this.props.url}>{this.props.title}</Link>}
```

親ページでは、URLを渡すように設定します。

```diff:src/App.tsx
              <SummaryItem
                key={data.id}
+               url={`./contents/${data.id}`}
                title={data.title}
                date={data.date}
                abstract={data.abstract}
```

これでトップページからのリンクはできました。
では、詳細ページを作っていきます。


作り方は、App.tsxと同じでページ(ContentPage.tsx)とViewModel(ContentPageStore.ts)を作って
MobXで連携させます。

![Object-Diagram3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/3615789e-e61c-3aa3-ad1a-b37ccd042749.jpeg)


```React:src/ContentPage.tsx　(新規作成)
import React from "react";
import { RouteComponentProps } from "react-router-dom";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import Typography from "@material-ui/core/Typography";
import { ContentPageStore } from "./ContentPageStore";
import { inject, observer } from "mobx-react";

interface ContentPageProps extends RouteComponentProps<{ id: string }> {
  contentPageStore?: ContentPageStore;
}

@inject("contentPageStore")
@observer
class ContentPage extends React.Component<ContentPageProps> {
  public componentDidMount = (): void => {
    if (this.props.contentPageStore === undefined) {
      return;
    }
    const id = parseInt(this.props.match.params.id, 10);
    this.props.contentPageStore.fetchData(id);
  };

  render(): JSX.Element {
    if (this.props.contentPageStore === undefined) {
      return <div />;
    }
    const store = this.props.contentPageStore;
    return (
      <div className="App">
        <Card style={{ width: 300, margin: 2 }}>
          <CardHeader title={store.data.title} subheader={store.data.date} />
          <CardContent>
            <Typography color="textSecondary" gutterBottom>
              {store.data.content}
            </Typography>
          </CardContent>
        </Card>
      </div>
    );
  }
}

export default ContentPage;
```

```react:src/ContentPageStore.ts　(新規作成)
import { action, makeAutoObservable, observable } from "mobx";

interface BlogContent {
  id: number;
  title: string;
  date: string;
  content: string;
}

export class ContentPageStore {
  constructor() {
    makeAutoObservable(this);
  }
  @observable
  data: BlogContent = {
    id: 0,
    title: "",
    date: "",
    content: ""
  };

  @action
  public fetchData = async (id: number): Promise<void> => {
    const json = (await (
      await fetch(`/api/contents/${id}`)
    ).json()) as BlogContent;
    this.data = json;
  };
}
```

React-Routerの本番はここからです。
今まで`index.tsx`で`<App/>`と直接Reactコンポーネントを指定していたところを、
`<BrowserRouter>`で囲って、`<Route/>`を並べていきます。
`<Route>`の`component=`にReactコンポーネントを、`path=`にURLを指定します。

また、ViewModelもStoresに追加します。

```diff:src/index.tsx
  import fetchMock from "fetch-mock";
+ import ContentPage from "./ContentPage";
+ import { ContentPageStore } from "./ContentPageStore";
+ import { BrowserRouter, Route } from "react-router-dom";


  const stores = {
-   appStore: new AppStore()
+   appStore: new AppStore(),
+   contentPageStore: new ContentPageStore()
  };
  ReactDOM.render(
    <Provider {...stores}>
-     <App />
+     <BrowserRouter basename={"/"}>
+       <Route exact={true} path="/" component={App} />
+       <Route exact={true} path="/contents/:id" component={ContentPage} />
+     </BrowserRouter>
    </Provider>,
    document.getElementById("root")
  );

```

これで、`/`の場合はAppのページが、`/contents/(id)`の場合は、ContentPageのページが表示されるようになります。
URLのルートURLは、`<BrowserRouter basename=`に指定します。

昔ながらのCGIをやっていた方は、URLが違うなら別のhtmlが返ってくるのでは？という気がしますが、
SPAの場合は、バックエンド側で同じindex.htmlを返すように設定するようです。

一応これで完成なのですが、相変わらずバックエンドがないので
ダミーデータを返すようにします。

```diff:src/index.tsx
-     abstract: "アブストラクトです。"
+     abstract: "アブストラクトです。",
+     content: "これは本文です。"
    },
    {
      id: 2,
      title: "[サーバー]タイトルその2",
      date: "2019-12-17",
-     abstract: "アブストラクトその2です。"
+     abstract: "アブストラクトその2です。",
+     content: "これは本文です。"
    }
  ];
  fetchMock.get("/api/summaries/", dummyData);
+ fetchMock.get("/api/contents/1", dummyData[0]);
+ fetchMock.get("/api/contents/2", dummyData[1]);
```

以上で、簡単ですがブログっぽいページができました。
TOPページからリンクをたどると、詳細ページに飛びます。
URLも変化していますので、F5を押しても元ページに戻ったりしません。

![React-blog-top-withLink.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/7243aba8-9f57-1e9d-6250-fffbf8fde04c.jpeg)

![React-blog-content.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217768/fd86903c-799b-7d86-2c34-0397f2eb050f.jpeg)

## ビルドしてデプロイ可能にする

ある程度アプリができたら、ビルドしてデプロイできるようにします。
次のコマンドを実行すると、buildフォルダに成果物が出来上がります。

```shell:commandline
npm run build
```


# その他細かいTips

## 多言語対応は？

Javascript用にいくつかライブラリがあるようです。

* [i18next](https://www.i18next.com/)
* [polyglot](https://github.com/airbnb/polyglot.js)

基本的には、言語ごとにJsonを用意して、キーとなる文字列を指定すると、
実行時には言語に応じたJsonから文字列が取れるようです。

```json:リソースの定義
{
  "buttons":{
      "new":"新規投稿"
      "edit":"編集"
  }
}
```

```react:多言語リソースを使う例
   <Button label={i18next.t('buttons.new')} />
```

C#のResources.resxと比較すると、型がないので、インテリセンスが使えない分、ちょっと面倒です。

以前やったときに、Typescript＋Jsonスキーマを使うといい感じに改善できそうだったので、
また記事を書くかもしれません。

## サーバーサイドはどうする？

Node.jsもいいですが、C#erなら、過去のC#の資産を使えるASP.NET Coreを使いたいです。

Visual Studioで[プロジェクトの新規作成]-[ASP.NET Core Webアプリケーション]→[React.js]で
テンプレートを作って、ClientApp以下を上と同じ手順で作ったReact開発環境で置き換えれば、
いいようです。

普通にF5実行で、Reactのフロントエンド＋C#のバックエンドで起動しました。
なお、初回はビルドが走るのか重いようです。

問題は、ASP.NET Coreを動かすサーバーを用意できるかどうか。

## ReactとWindows Formで開発速度はどれくらい違う？

htmlが不慣れとかいろいろ要素がありますが、
私の場合、Windows Formで作るほうが3倍くらい速いです。

C# ←→ Typescriptの変換も面倒ですし、
画面レイアウトの作りこみも大変です。
というか、C#を振り返ると、Visual StudioのWindows Formのデザイナーはよくできているなぁと思います。
(壊れやすいのが難点ですが)

# まとめ

割とがっつりReactの入門記事になってしまいましたが、
普段C#を使っている方の視点で、解説したつもりです。
Reactを使ってSPAを始めるための一通りを説明したつもりなので、
あとは応用を学んでいけば、そこそこのプロダクトが作れるのではないでしょうか。

なお、上で作ったコードは、一応GitHubに公開しておきます。
(この記事の賞味期限が有効なあたりまでは残しておきます)
https://github.com/banban525/react-example-2019

自分で打ち込むのが面倒な方はそちらを見てもらってもよいと思います。
個人的には写経はおすすめですが。