create-react-appからの脱却 - TypeScript,Reduxの導入(RxJS6も

コード書き始めてから1年くらいの[Noob](https://www44.atwiki.jp/bf_3/pages/47.html#id_9028fbe5)です。ぼちぼち、`create-react-app`にまかせっきりのスタイルから脱却を図ります。ついでに、JavaScript(以下js)で静的型付けを実現するTypeScript(以下ts)、状態管理周りのごちゃごちゃも一掃するためのRedux、それで非同期処理を行うためのRxJS6も導入します。

`create-react-app`を`eject`し、とりあえずtsとReduxを使った簡単なページを作成します。

## 学ぶこと
- TypeScriptの書き方
- Reduxの基本的な使い方
- Reduxでの非同期処理(redux-observable)
- ReactとTypeScriptとReduxの連携

この記事の対象読者としては、`create-react-app`で何か作ったことがあって、jsについて基本だけはわかるという人を想像しています。基本的に読んだ記事へのリンクを至るところに貼ってあるので、すべて読めば最低限自分と同じくらいにはなると思います。

### 環境
- npm -v 6.4.1

npmは[npx](https://qiita.com/tonkotsuboy_com/items/8227f5993769c3df533d)が使えるバージョンであれば良いです。またパッケージ管理には[Yarn](https://ics.media/entry/19384)を使っています。かわいいので。いやな人は適宜npmに[置き換えて](https://qiita.com/morrr/items/558bf64cd619ebdacd3d#npmとの対応)ください。

## Let's get started
[ジャバ・ザ・ハットリさんの記事](https://qiita.com/jabba/items/5ebea8d19809a4e0f8c6)に書かれているReact+Reduxのサンプルと同じ内容を、TSで書き換えたり、非同期処理を切り出したりすることで学んでいきます。Reduxについてかなりわかりやすい記事なので、一読することをおすすめします。

```console
npx create-react-app test-ts --scripts-version=react-scripts-ts
cd test-ts
yarn eject
yarn add redux react-redux
yarn add --dev @types/react-redux
yarn start // 上手く起動するかテスト
// 表示が確認できたら閉じる
```

react-reduxはReactとReduxを繋いでくれるライブラリです。@types/react-reduxは、react-reduxの型が定義されたファイルで、tsには必須です。

さらに、tsでReduxを使う上で、[いろいろ楽にしてくれる](https://qiita.com/m0a/items/703d64c74e43cb392a64)ライブラリを追加します。[typescript-fsa](https://github.com/aikoven/typescript-fsa)と[typescript-fsa-reducers](https://github.com/dphilipson/typescript-fsa-reducers)です。

```console
yarn add typescript-fsa typescript-fsa-reducers
```

## まずTypeScriptについて概要を押さえておく
これからいきなりtsを書いていくので、一応概要は押さえておく必要があります。以下を参考にしてください。

- [初心者が学ぶ TypeScript 入門 Ver.1.0](https://qiita.com/ikkitang/items/9acaf6a166f771e5ce6f)
入門。さらっと読んでtsなんぞ、の理解を深める。

- [関東最速でReact+Redux+TypeScriptなアプリを作る](https://qiita.com/IzumiSy/items/b7d8a96eacd2cd8ad510)
tsとReactの連携はこうやるのかぁ的に、大枠を理解する。この記事もこちらを参考にした箇所多し。

- [TypeScriptの型入門](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a)
tsのチートシート。量があるので全ては読まず、わからない箇所が出てきたら適宜振り返る形式で良いが、[オブジェクト型](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a#オブジェクト型)ぐらいまではあからじめ知っておくと便利かも。

おおまかに理解したら、とりあえず進みましょう。とりあえず記事通りに書けば動きはしますので。

## Reduxについての理解と導入
[「React+Redux入門」](https://qiita.com/erukiti/items/e16aa13ad81d5938374e)と[「Reduxの実装とReactとの連携を超シンプルなサンプルを使って解説」](https://mae.chab.in/archives/2885)を読んで、Reduxの概要を理解します。細かい実装に関しては、今回は記事と違いtsである、また記事が多少古いことも鑑みて気にしなくてよいです。Reduxとはどういうものかについてコードベースで大まかに理解できたら、[今回の目標サンプルコード](https://qiita.com/jabba/items/5ebea8d19809a4e0f8c6)再度見返します。何をやっているのか理解できるはずです。

Reduxを導入することで、viewがaction(APIを叩くなど)と分離して綺麗になるなぁと感じてきたら、いい頃合いです。

個人的な理解を整理すると、

- storeを介してstateが管理されている（1つのアプリに1つのstore）
- reducerでstoreのstateの遷移を行う（stateに触れられるのはここ）
- actionがフロントで叩かれることでreducerが反応する
- containerがreduxと連携し、componentはviewのみに徹する

ここでしっかりReduxについて理解しておかないと、今回はtsもReactive Xなんて概念も盛り込んでいるので、後々辛くなります(というか自分はなりました笑)。分かんなくなったら、適宜振り返ります。

そこまでいったら、実際に書いていきます。まずは[こちら](https://qiita.com/jabba/items/5ebea8d19809a4e0f8c6#念のためreduxの３原則)を参考にフォルダを作成します。

```console
mkdir src/{actions,reducers,containers,components}
touch src/{containers,components}/Comments.tsx
touch src/{actions,reducers}/comments.ts
touch src/reducers/index.ts
touch src/store.ts
mkdir src/api
touch src/api/getComments.ts
```

### Actionを作成する
tsを使う場合では型定義が必要になるので、普通にactionを書くと型定義も含め[かなり煩雑になりそう](https://blog.mitsuruog.info/2018/03/react-redux-observable-typescript#ActionとConstants)です。そこで、[typescript-fsa](https://github.com/aikoven/typescript-fsa#usage)を使います。

これによりactionを簡単に書け、その呼び出しも楽になります。

```src/actions/comments.ts
import actionCreatorFactory from 'typescript-fsa';
import { IComments } from '../api/getComments';

const actionCreator = actionCreatorFactory();

const commentsActions = {
  fetch: actionCreator.async<{ url: string }, { comments: IComments[] }, { hasError: boolean }>('FETCH_COMMENTS'),
  loading: actionCreator<{ isLoading: boolean }>('LOAD_COMMENTS'),
};

export default commentsActions;
```

asyncな場合のactionに関しては[公式ドキュメントのこの辺](https://github.com/aikoven/typescript-fsa#async-action-creators)に書かれています。また、apiの箇所に関してはあとで作るので今は無視しておいてください。

### Reducerを作成する
次にreducerです。ここでstateの遷移が行われるため、stateの初期値の定義などもここで行います。[typescript-fsa-reducers](https://github.com/dphilipson/typescript-fsa-reducers#usage)を使うことで、条件分岐を執拗に書かなくて良くなるので導入しています。

```src/reducers/comments.ts
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import commentsActions from '../actions/comments';
import { IComments } from '../api/getComments';

export interface ICommentsState {
  hasError: boolean;
  isLoading: boolean;
  comments: IComments[];
}

const initialState: ICommentsState = {
  hasError: false,
  isLoading: false,
  comments: [],
};

export const commentsReducer = reducerWithInitialState(initialState)
  .case(commentsActions.loading, (state, payload) => {
    return Object.assign({}, state, { isLoading: payload.isLoading });
  })
  .case(commentsActions.fetch.done, (state, payload) => {
    return Object.assign({}, state, { comments: payload.result.comments })
  })
  .case(commentsActions.fetch.failed, (state, payload) => {
    return Object.assign({}, state, { hasError: payload.error.hasError })
  })

export default commentsReducer;
```

ここの`ICommentsState`は[interface](https://qiita.com/nogson/items/86b47ee6947f505f6a7b)として後から利用するので`export`しておきます。大文字のIが付くのは、interfaceを宣言する時の慣習のようなもので、これをやらないとtslintで怒られることがあります。

Object.assignについては[MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#オブジェクトをマージする)を参照してください。ここで`Object.assign(state, newState)`とせずに`Object.assign({}, state, newState)`としている理由は、元のstateを"上書き"するのではなくあくまで"変更"する必要があるからです。"変更"にすることで、Reduxがその変更を検知し、ReactがViewに反映します。

この`.case`は[typescript-fsa-reducers](https://github.com/dphilipson/typescript-fsa-reducers#introduction)に含まれるメソッドです。これにより、煩雑な条件分岐を書かなくてよくなります。`.case`内の`isLoading`や`comments`、`hasError`には、後で書く処理によりactionのpayloadが代入されます。payloadは、asyncな処理が成功した場合にのみ[少し異なる](https://github.com/aikoven/typescript-fsa#async-action-creators)ため注意が必要です。

また、Reducerが増えた時に備え、それらをまとめる"reducers/index.ts"も作っておきます。

```src/reducers/index.ts
import { combineReducers } from 'redux';
import { ICommentsState, commentsReducer } from './comments';

export interface IAppState {
  comments: ICommentsState
}

export const reducers = combineReducers<IAppState>({ comments: commentsReducer })

export default reducers;
```

ここの`IAppState`が、storeに格納するstateになります。これをstore経由で個別のcontainerに伝え、その中で必要なstateのみを展開することでcomponentにpropsが伝わります。

### Storeを作成する
storeはreducersから作成するだけで良いので簡素です。

```src/store.ts
import { createStore } from 'redux';
import reducers from "./reducers/index";

const store = createStore(
  reducers
);

export default store;
```

このstoreをReactのcontainerが直接的に参照することで、propsが伝わります。

### ReactとReduxの連携
ReactがReduxと関係を持つ場所としては、srcフォルダ以下のメインとなる"index.tsx"とcontainerの2つがあります。

["index.tsx"とReduxの連携](https://qiita.com/erukiti/items/e16aa13ad81d5938374e#reactとreduxをつなぐ)は簡単です。`react-redux`の`Provider`を使って`store.ts`を流し込むだけ。

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

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

`<Provider store={store}>`の箇所でstoreを流し込んでいます。App.tsx自体はcontainerをラップしているだけのcomponentです。

```src/App.tsx
import * as React from 'react';
import Comments from './containers/Comments';

const App = () => {
  return (
    <div>
      <Comments />
    </div>
  )
}

export default App;
```

次に[Reduxとcontainerの連携](https://qiita.com/erukiti/items/e16aa13ad81d5938374e#container)です。

ここで連携するのは、「Reduxのstoreで管理されているstate」と「stateに変更を加えるためのreducerを操作するaction」の2つです。

```src/containers/Comments.tsx
import * as React from 'react'
import { Dispatch } from 'redux';
import { connect } from 'react-redux'
import { AppState } from '../reducers/index'
import commentsActions from '../actions/comments'
import * as AppComponent from '../components/app'

const mapStateToProps = (appState: AppState) => {
  return Object.assign({}, appState.comments);
};

const mapDispatchToProps = (dispatch: Dispatch<any>) => {
  return {
    fetchData: (url: string) => dispatch(commentsActions.fetch.started(url))
  }
};

// Container
const App = connect(
  mapStateToProps, mapDispatchToProps
)(AppComponent)

export default App;
```

containerには、[react-reduxのconnect](https://qiita.com/MegaBlackLabel/items/df868e734d199071b883#connect)により、storeからstateが伝わります。そのstateは先ほど"src/reducers/index.ts"の箇所で説明した、`IAppState`の型を取ります。つまり、以下のような想定です。

```appState
// src/containers/Comments.tsx内のappState
{
  comments: {
    hasError: boolean;
    isLoading: boolean;
    comments: IComments[];
  }
}

// なぜなら、appStateはsrc/reducers/index.tsにて以下のような型で定義されており
interface IAppState {
  comments: ICommentsState
}

// src/reducers/comments.ts内でICommentsStateは以下の型と定義されているから
interface ICommentsState {
  hasError: boolean;
  isLoading: boolean;
  comments: IComments[];
}
```

このように型定義されていると、オブジェクトが深くなっても正確に追えるので便利だなぁと、ここで感じました。

`mapStateToProps()`では、commentsのcontainerで必要なstateのみ、つまり`appState.comments`を取り出しています。

`mapDispatchToProps()`では、comments containerに関係するactionのみをstateに入れます。`dispatch(action)`とすることで、そのactionを割り当てることができます。ここでは、componentで`fetchData()`を実行すると、`commentsActions.fetch.started()`というactionに基づきstateが変化するようになります。

それらを`connect()()`で、状態を持たない純粋なcomponentと結びつけてcontainerは完了です。

もしReactから上がってきた値を直接的にstateに入れるのではなく、何か手を加えたいのであれば、この`mapDispatchToProps()`内で加工するのも1つの手です(例えば、正規表現で余分な箇所を削るなど)。

以上で、基本的な箇所は完了です。しかし、Reactのcomponentが受け取った値を使用して非同期処理などの複雑な処理を行いたい場合は別途、どうにかする必要があります。

## Reduxでの非同期処理
今回問題となるのが、APIからコメントデータを取ってくる`fetchComments()`です。これを実装するためには、Reduxにおける非同期処理の方法を学ぶ必要があります。[こちらの記事](https://qiita.com/ymr-39/items/63386fd65c7e9f06f5d4)などを参考にしたところ、どうやらReduxでは、非同期処理を実装する方法として以下の4つが考えられるようです。

- コンポーネント内に直接書く
- ReduxのActionに書く（redux-thunk）
- Sagaという別のプロセスに組み込む（redux-saga）
- RxJSを噛ませる（redux-observable）

大した処理がないものでは[componentに組み込む方法](https://qiita.com/IzumiSy/items/b7d8a96eacd2cd8ad510#containerでのapiアクセス処理の実装)がベストですが、勉強も兼ねて[redux-observable](https://redux-observable.js.org/)を使ってみます。最初は[redux-saga](https://redux-saga.js.org/)が、[完全に別のプロセスに分離できそう](https://qiita.com/kuy/items/716affc808ebb3e1e8ac)でいいなぁと思っていたのですが、[この記事](https://qiita.com/terrierscript/items/c672ddad4c24936b1061)を読んで「RxJSヤバくね？」と単細胞にも感じたので、redux-observableに決定。

### redux-observableの導入
[redux-observale](https://redux-observable.js.org/)は、Reduxのactionが実行されたときに行う複雑な処理、特に非同期処理をRxJSで処理するために必要となるライブラリです。Reduxのミドルウェアとして機能します。

導入にあたり、[typescript-fsa-redux-observable](https://github.com/m0a/typescript-fsa-redux-observable)を導入するか否かはどっちでも。これを導入することでの[旨味は多い](http://labs.septeni.co.jp/entry/2018/02/08/090000#h5o-12)ようなので、入れてもいいかもしれません。今回は入れません。

また、[公式の注意書き](https://redux-observable.js.org/docs/basics/Epics.html)にもありますが、redux-observableを使うためにはRxJS v6について多少の知識が必要です。RxJsについては、[このLIGの記事](https://liginc.co.jp/web/js/151272)で何となく凄いライブラリだなぁ感じ取ってから、[Qittaの逆引きredux-observable](https://qiita.com/tsugitta/items/03020a0f501c15c37d32)で概要を掴みました。一番参考になったのは、リクルートのエンジニアブログの[RxJSを学ぼう](http://bit.ly/2E45Ahb)です。一通り軽く読み流すと、わかってきます。また、WEB上のドキュメントはRxJS v5.5以下の情報が多いですが、RxJS v6では`.pipe`使う必要があったりと前バージョンからの[破壊的な変更](https://qiita.com/ponday/items/7966787fa0bc8f3e2dde)が多いです。実装する際は、RxJSのバージョンが違うドキュメントを鵜呑みにすると動かないので注意です。

追々、[RxJS公式ドキュメント](https://rxjs-dev.firebaseapp.com/guide/overview)も読んでいきたいと思いますが、まずは実装してみます。実装にあたっては、[redux-observableのドキュメント](https://redux-observable.js.org/docs/basics/Epics.html)、および[typescript-fsaのドキュメント](https://github.com/aikoven/typescript-fsa#redux-observable)を参考にして進めます。

RxJSのajax関数を使った非同期処理が[公式では紹介されています](https://redux-observable.js.org/docs/basics/Epics.html#a-real-world-example)が、[`fetch`でPromiseを返し、それをObservableに変換する](https://blog.mitsuruog.info/2018/03/react-redux-observable-typescript#Epic)こともできます。お好きなほうを。今回はAPIを切り出す方法で行きます。では必要なファイルなどを導入します。

```console
yarn add redux-observable rxjs
mkdir src/epics
touch src/epics/{comments.ts,index.ts}
mkdir src/api
touch src/api/getComments.ts
```

### まずはAPI部分
特に変わったところはありません。APIは[ここで使われている](https://qiita.com/jabba/items/5ebea8d19809a4e0f8c6#apiからデータを取ってくる)もので、コメントをjsonで撮ってきます。`IComments`はそのinterfaceです。APIに関わることなので、ここで型定義をしています。

```src/api/getComments.ts
export interface IComments {
  id: string;
  comment: string;
}

const getComments = async (url: string) => {
  return await fetch(url)
    .then(response => response.json())
    .then(comments => comments)
    .catch((e: string) => e);
}

export default getComments;
```

### Epicを作成する
redux-observableのコア部分であるEpicを作成していきます。

```src/epics/comments.ts
import { Action } from 'redux';
import { of, from, concat } from 'rxjs';
import { map, mergeMap, /*catchError*/ } from 'rxjs/operators';
import { Epic, ofType } from 'redux-observable';
import { IComments } from '../api/getComments';
import commentsActions from '../actions/comments';
import getComments from '../api/getComments';

interface IPayloadAction extends Action {
  type: string;
  payload?: any;
}

const commentsFetchEpic: Epic<IPayloadAction> = (actions$) => actions$.pipe(
  ofType(commentsActions.fetch.started.type),
  mergeMap((action: IPayloadAction) => concat(
    of(commentsActions.loading({ isLoading: true })),
    from(getComments(action.payload.url)).pipe(
      map((comments: IComments[]) => commentsActions.fetch.done({ params: action.payload.url, result: { comments } })),
      // TODO: 以下ではError handlingができない
      // catchError(error => of(commentsActions.fetch.failed({ params: action.payload.url, error: { hasError: true } }))),
    ),
    of(commentsActions.loading({ isLoading: false })),
  )),
);

export default commentsFetchEpic;
```

まず`IPayloadAction`ですが、これはEpicが受け取るReduxのactionのinterfaceになります。これを型定義しておくことで、`Property 'payload' does not exist`などのような[エラーを避ける](https://github.com/ngrx/platform/issues/31#h5o-14)ことができます。

`ofType()`は`action.type === 'ACTION_TYPE'`の[redux-observableにおけるシンタックスシュガー](https://redux-observable.js.org/docs/basics/Epics.html#a-basic-example)です。

`mergeMap`(`flatMap`と同じもの)は、[APIを呼び出す際](https://christina04.hatenablog.com/entry/2017/02/25/111715)などに使います。ここでは`mergeMap`内で複数のactionを呼び出したかったので、`concat()`で繋げています。[`concat()`](https://www.learnrxjs.io/operators/combination/concat.html)は複数のobservableを1つのobservableにします。[`from()`](https://www.learnrxjs.io/operators/creation/from.html)は、PromiseやArrayをobservableに変換する役割です。ここでは`getComments()`APIをobservableにして`.pipe()`で流し、[`map()`](https://www.learnrxjs.io/operators/transformation/map.html)でactionを実行します。asyncなactionの取り扱いについては、[typescript-fsaのドキュメント](https://github.com/aikoven/typescript-fsa#async-action-creators)より確認してください。

この非同期処理の全体の流れは[このissue](https://github.com/redux-observable/redux-observable/issues/62)を参考にしており、それによると[`startWith()`や`endWith()`を使った解決方法](https://github.com/redux-observable/redux-observable/issues/62#h5o-32)のほうがコードの透視は良さそうです。しかし、`endWith()`で`commentsActions.loading({ isLoading: false })`を実行しようとしても上手くいかなかったため、[素直に流して](https://github.com/redux-observable/redux-observable/issues/62#h5o-31)います。

自分はこのRxJSの箇所でかなり詰まりました。RxJS難しい。。。

~~いまだ、[エラーハンドリングが上手くいかずに困っている](https://teratail.com/questions/161020)ので、どなたかお助け願います！！~~

結局、`ajax`を使うことで解決しました。[公式](https://github.com/redux-observable/redux-observable/blob/master/docs/recipes/ErrorHandling.md)にもその方法が載っていた。。

```src/epics/comments.ts
ajax.getJSON(action.payload.url).pipe(
      map((comments: IComments[]) => commentsActions.fetch.done({ params: action.payload.url, result: { comments } })),
      catchError(_ => of(commentsActions.fetch.failed({ params: action.payload.url, error: { hasError: true } }))),
    ),
```

それにともない、"api/getComments"は削除してよいです。

### Epicsをまとめる
他に処理が増えるにしたがいEpicsも増えていくため、別途結合し、それをstoreに流しこみます。

```src/epics/index.ts
import { combineEpics } from 'redux-observable';
import commentsFetchEpic from './comments';

const epics = combineEpics(
  commentsFetchEpic
);

export default epics;
```

[combineEpics](https://redux-observable.js.org/docs/basics/Epics.html#combining-epics)は複数のEpicをまとめてくれます。これを、storeにミドルウェアとして流し込みます。

```src/store.ts
import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import reducers from "./reducers/index";
import epics from './epics/index';

const epicMiddleware = createEpicMiddleware();

const store = createStore(
  reducers,
  applyMiddleware(epicMiddleware)
);

epicMiddleware.run(epics);

export default store;
```

ここは[公式ドキュメント](https://redux-observable.js.org/docs/basics/SettingUpTheMiddleware.html)の通りです。

これで完成となります。`yarn start`で起動したら、コメントが表示される。。と思いきや、tslintの設定で怒られるので、設定を少しいじってみます。

## tslint関連の設定を少し直す
起こられたままだと上手く表示できないので、直すか設定を変更するかします。[「interfaceのnameはIで始まらなければならない」](https://asahima.hatenablog.jp/entry/2018/09/20/220000)、「objectリテラルとか、ショートハンド使えよ」
(`{comments: comments}`ではなく、`{ comments }`使えよってこと)、[「typeよりinterface使えよ、普通は」](https://github.com/Microsoft/TypeScript/issues/14736)、以上の3つに関しては1度怒られたのですが、良さそうなlintだったので準拠しました。

### tslint:「impotなどをアルファベット順に書けよ」をオフにする
アルファベット順にimportが書かれていないと、`Import sources within a group must be alphabetized.`のようなエラーが大量発生します。[こいつ](https://palantir.github.io/tslint/rules/ordered-imports/)が効いているようです。

あまり自分は重要性を感じなかったので、オフにしました。

```tslint.json
{
  "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
  "linterOptions": {
    "exclude": [
      "config/**/*.js",
      "node_modules/**/*.ts",
      "coverage/lcov-report/*.js"
    ]
  },
  "rules": {
    "ordered-imports": [false],
    "object-literal-sort-keys": [false]
  }
}
```

以上で本当に完了です。問題なければ、`yarn start`で以下のような感じに表示されるはずです。

![ts.png](https://qiita-image-store.s3.amazonaws.com/0/225665/4f043f48-56a4-57f2-1783-aa85fada9fc6.png)