関東最速でReact+Redux+TypeScriptなアプリの開発環境を作る

とりあえず関東最速でReactアプリをTypeScriptで作るためのレシピです

**注1: このサンプルではReduxを使いますが、もしImmutabilityを必要としないのであればUnduxを代替に検討してみてください！ Reduxは関数型エッセンスを守るために非常にファイル数が多くなります。 →[【Reduxに疲れた人のための】Undux入門](https://qiita.com/IzumiSy/items/c26f69219178b79f586d)**

**注2: React+Reduxはデフォルトの最小構成でもファイル数が多く複雑、かつTypeScriptで型を付けても完全に実行時エラーをゼロにするのは不可能です。よりアプリケーションの質を高めるためには、Reduxの起源となったElmの利用を検討してみてください→[関東最速でElm+JSなアプリの開発環境を作る](https://qiita.com/IzumiSy/items/4aff1db5c773c48ef50b)**

# create-react-app
最速で作るにはcreate-react-app一択です。`--typescript`オプションが利用できるので、それを使います

```console:create-react-appを使って雛形を生成
$ npx create-react-app ts-react-app --typescript
```

# redux

まずreduxをインストール

```console:Reduxを導入
$ npm install --save redux react-redux
```
型定義ファイルを入れるのも忘れずに

```console:react-reduxの型定義ファイルのインストール
$ npm install -D @types/react-redux
```

## typescript-fsa
最速で作るにあたってReduxのボイラープレート・コードを書くのはダルすぎます。
なのでtypescript-fsaを入れましょう。

```console:typescript-fsaを導入
$ npm install --save typescript-fsa typescript-fsa-reducers
```
これを使うことでFSA(Flux Standard Action)[^1]にも準拠できます。楽ですね。

[^1]: https://github.com/acdlite/flux-standard-action

さて早速コードを書いていきましょう

## Action実装
`src/actions`でディレクトリを切ってこんな感じでActionを定義

```ts:hogeAction.ts
import actionCreatorFactory from 'typescript-fsa';

const actionCreator = actionCreatorFactory();

export const hogeActions = {
  updateName: actionCreator<string>('ACTIONS_UPDATE_NAME'),
  updateEmail: actionCreator<string>('ACTIONS_UPDATE_EMAIL')
};
```

## Reducer実装
`src/states`でディレクトリを切ってこんな感じでReducerを定義

```ts:hogeState.ts
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { hogeActions } from '../actions/hogeActions';

export interface HogeState {
  name: string;
  email: string;
}

const initialState: HogeState = {
  name: '',
  email: ''
};

export const hogeReducer = reducerWithInitialState(initialState)
  .case(hogeActions.updateName, (state, name) => {
    return Object.assign({}, state, { name });
  })
  .case(hogeActions.updateEmail, (state, email) => {
    return Object.assign({}, state, { email });
  });
```

## Store実装
`store.ts`みたいなファイルを`src`直下にでも置いて、`createStore`でストアを定義。

```ts:store.ts
import { createStore, combineReducers } from 'redux';
import { hogeReducer, HogeState } from './states/hogeState';

export type AppState = {
  hoge: HogeState
};

const store = createStore(
  combineReducers<AppState>({
    hoge: hogeReducer
  })
);

export default store;
```

# コンポーネント実装
今回はPresentational/Containerコンポーネント[^2]のパターンで実装を進めます。

PresentatonalとContainerは責務の観点から粗結合性を担保するためにビューをふたつのレイヤに分けるものです。具体的には以下のような違いがあります。

|       | Container|Presentational|
|:-----:|:---:|:---:|
| **役割**   | 「*振る舞い*」を実装 | 「*見た目*」を実装 |
| **状態** | 持つ | 持たない |
| **Reduxへの依存** | ある | ない |

すなわち、ContainerでReduxとの依存（ステートの状態やアクションの実行）などをラップし、ComponentはContainerによってラップされた振る舞いのみを知るだけでよくなります。

[^2]: https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

## Container実装
`src/containers`でディレクトリを切ってこんな感じで実装

```ts:hogeContainer.ts
import { Action } from 'typescript-fsa';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { AppState } from '../store';
import { hogeActions } from '../actions/hogeActions';
import { HogeComponent } from '../components/hogeComponent';

export interface HogeActions {
  updateName: (v: string) => Action<string>;
  updateEmail: (v: string) => Action<string>;
}

function mapDispatchToProps(dispatch: Dispatch<Action<string>>) {
  return {
    updateName: (v: string) => dispatch(hogeActions.updateName(v)),
    updateEmail: (v: string) => dispatch(hogeActions.updateEmail(v))
  };
}

function mapStateToProps(appState: AppState) {
  return Object.assign({}, appState.hoge);
}

export default connect(mapStateToProps, mapDispatchToProps)(HogeComponent);

```
`mapDispatchToProps`の中で`bindActionCreators`を使うのをやめましょう。それを使ってしまっては直接コンポーネントのなかで`dispatch`を呼んでいるのと同じで、Containerレイヤを置く意味がありません。もしそうしたいのであれば、Mobxなどを使ったほうがよいです。

## Component実装
Presentationalなコンポーネントには状態を与えるのをやめましょう。ロジックはContainer側で定義すべきです。

コードは`src/components`みたいなディレクトリを切ってそこへ入れましょう。ここは原則として状態を**持たない**場所なので、`React.SFC`を使うのが適切です。

```tsx:hogeComponent.tsx
import * as React from 'react';
import { HogeState } from '../states/hogeState';
import { HogeActions } from '../containers/hogeContainer';

interface OwnProps {}

type HogeProps = OwnProps & HogeState & HogeActions;

export const HogeComponent: React.SFC<HogeProps> = (props: HogeProps) => {
  return (
    <div>
      <div className="field">
        <input
          type="text"
          placeholder="name"
          value={props.name}
          onChange={(e) => props.updateName(e.target.value)}
        />
      </div>
      <div className="field">
        <input
          type="email"
          placeholder="email"
          value={props.email}
          onChange={(e) => props.updateEmail(e.target.value)}
        />
      </div>
    </div>
  );
};
```

## `App.tsx`への追加
`src`以下に`App.tsx`というファイルがあるので、そこで`HogeComponent`を表示するようにしましょう

```tsx:App.tsx
import * as React from 'react';
import HogeContainer from '../src/containers/hogeContainer';

class App extends React.Component {
  render() {
    return (
      <div className="App">
        <HogeContainer />
      </div>
    );
  }
}

export default App;
```

# `index.tsx`の更新
`src`以下にある`index.tsx`が実際にReactのrootコンポーネントをマウントしているところです。
最後にここでstoreを使うように書き換えましょう。

```tsx:index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import Store from './store';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import './index.css';

ReactDOM.render(
  <Provider store={Store}>
    <App />
  </Provider>,
  document.getElementById('root') as HTMLElement
);

registerServiceWorker();
```

# 起動
あとは以下のコマンドで起動します。

```console
$ npm start
```
(tslintでエラーが出る場合にはコメント欄にある@manomuさんのコメントを参考にしてみてください）

# 【発展編】非同期処理したい
以上であらかたのアプリケーションの骨組みは作れたものの、もっとちゃんとしたアプリケーション開発ではAPIを叩いたりしますよね。

typescript-fsaを使っているので、Actionは単なるステート更新の起点としての責務に徹し、APIを叩く処理はコンポーネントに対して振る舞いを定義するContainerの中[^3]に置きます。

[^3]: いやいや、そこはredux-sagaっしょ！ という人もいるかもしれませんが、ここでは省略します。

## 非同期Actionの定義
typescript-fsaに用意されている`actionCreator.async`というメソッドを使うと、内部で`STARTED`, `DONE`, `FAILED`というtypeサフィックスをつけたActionの定義を行ってくれます。

```ts:
import actionCreatorFactory, { ActionCreator, Success, Failure } from 'typescript-fsa';

const actionCreator = actionCreatorFactory();

const submit =
  actionCreator.async<{}, {}, {}>('ACTIONS_SUBMIT')

export interface HogeAsyncActions {
  startLogin: ActionCreator<{}>;
  failedLogin: ActionCreator<Failure<{}, {}>>;
  doneLogin: ActionCreator<Success<{}, {}>>;
}

export const hogeAsyncActions = {
  startLogin: submit.started,
  failedLogin: submit.failed,
  doneLogin: submit.done
}
```

## Reducerの定義
ReducerはActionで定義された３つのステートを処理するcaseを作るだけです。

```ts:hogeAsyncState.ts
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { hogeAsyncActions } from '../actions/hogeAsyncActions';

// ...

export const hogeAsyncReducers = reducerWithInitialState(initialState)
  .case(hogeAsyncActions.startLogin, (state) => {
    // ...
  })
  .case(hogeAsyncActions.failedLogin, (state) => {
    // ...
  })
  .case(hogeAsyncActions.doneLogin, (state) => {
    // ...
  })
```

## ContainerでのAPIアクセス処理の実装
上ですでに実装したContainer内のmapDispatchToPropsで以下のようにAPIアクセスを行う一連の処理を実装します。

```ts:hogeContainer.ts
// ...

import { hogeAsyncActions } from '../actions/hogeAsyncActions';

// ...


function mapDispatchToProps(dispatch: Dispatch<void>) {
  return {
    submit() {
      dispatch(hogeAsyncActions.startLogin({}));

      callYouOwnAPI()
        .then(() => {
          dispatch(hogeAsyncActions.doneLogin({
            params: {}, result: {}
          }));
        })
        .catch(() => {
          dispatch(hogeAsyncActions.failedLogin({
            params: {}, error: {}
          }));
        });
    },

    // ...
  };
}
```
あとはこれをコンポーネントの中から呼び出すだけです。

# tslintの設定

`tslint.json`に以下を追加

```json
"rules": {
  "interface-name": [
    false
  ],
  "object-literal-sort-keys": false,
  "ordered-imports": false,
  "no-empty-interface": false,
  "interface-over-type-literal": false
}
```