TypeScriptでRedux～そしてユニットテストへ～

　ドラゴンクエストⅢみたいなタイトルになってしまいましたが，わりと内容は真面目です。最近，仕事で本格的にReactとReduxを使う機会があったので，お手本構成プロジェクトを自分なりに作ってみました。備忘録がてらQiitaにも内容を残しておきたいと思います。今回の目的は以下の通りです。

- TypeScriptでReactとReduxを扱う
- (テストを考慮して)ReactとReduxは疎結合にする→Container/Presenter構成
- 非同期処理も考慮する→Redux Thunk導入
- ユニットテストを書く

## 何はなくともcreate-react-app
　手抜き目的でボイラープレートを使います。
だから，いつまでたってもwebpackが書けない。


```bash
npm install -g create-react-app
```

　TypeScriptモードでプロジェクトを生成しておきます。めちゃ便利。

```bash
create-react-app sample --typescript
```


## 生成されるコードをクラス仕様に変更
　ボイラープレートが生成するコードは，クラス仕様になっていないので変更しておきます。

```js

interface AppProperty {}
interface AppState {}

/**
 * 生まれ変わったAppコンポーネント
 */
export default class App<AppState, AppProperty> extends React.Component {
  render() {
    return (
      <div className="App">
      </div>
    )
  }
}
```

## Reduxを導入
　Reduxを導入します。ActionCreatorやReducerを作るのが面倒なので補助ライブラリも入れておきたいですね。

- typescript-fsa　→　ActionCreatorを簡単に生成してくださる素晴らしい方
- typescript-fsa-reducers　→　Reducerを簡単に作成してくださる素晴らしい方

```bash
npm i -S react-redux redux typescript-fsa typescript-fsa-reducers
```

　あと，忘れずにRedux用のTypeScript型ファイルも入れておきます。

```bash
npm i -D @types/react-redux
```

## redux thunkを導入
　非同期処理もやっていくのでthunkもこのタイミングで導入します。TypeScriptの型も忘れずに！

```bash
npm i -S redux-thunk
npm i -D @types/redux-thunk
```

## Actionの作成

### 普通のAction
　まずはObjectを返却する普通のActionです。

```js
import { actionCreatorFactory } from 'typescript-fsa';

interface AdviceInfo {
  message: string;
}

const actionCreator = actionCreatorFactory();
//普通のAction
export const fetchAdvice = actionCreator<AdviceInfo>('FETCH_ADVICE');
```

　typescript-fsaのおかげで簡潔に書けますね。

### 非同期処理のAction
　続いて非同期パターン。redux-thunkを使うので関数で書いていきます。今回はAxiosで格言？をランダムで返してくれるOpenなAPIを呼んでみました。
ちなみに，**middleware(redux-thunk)の適用を忘れると「Actionはオブジェクトしか返せねぇよ！！！(怒)」というエラーが出る**ので気をつけましょう。

```js
//非同期処理を行うAction
export const fetchAdviceAsync = () => {
  return async (dispatch: Dispatch<Action>, getState: () => AppState) => {
    try {
      const response = await axios.get('https://api.adviceslip.com/advice');
      const result: AdviceInfo = {
        message: response.data.slip.advice,
      };
      dispatch(fetchAdvice(result));
    } catch {
      //勇者よ，忘れず例外処理をやるのです
      //例外通知用の同期Actionを作るのもオススメです
    }
  };
};
```

　AppStateはStoreで定義します。念の為。

## Reducerの作成
　今度はActionを受けてStateの操作を行うReducerを作っていきます。typescript-fsa-reducersが大活躍です。

```js
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { fetchAdvice } from '../actions/adviceAction';

/**
 * ReducerのState型
 */
export interface AdviceState {
  message: string;
}

/**
 * ReducerのState初期値
 */
const initialState: AdviceState = {
  message: '',
};

/**
 * Actionを受け取った時のState操作
 */
export const adviceReducer = reducerWithInitialState(initialState).case(
  fetchAdvice,
  (state, payload) => {
    return {
      ...state,
      message: payload.message,
    };
  }
);
```

## Storeの作成
　ここは普通……ですね！　Thunkをmiddlewareとして適用するのを忘れずに！

```js
/**
 * アプリケーション全体のStore定義
 */
export interface AppState {
  advice: AdviceState;
}

const store = createStore(
  combineReducers<AppState>({
    advice: adviceReducer,
  }),
  applyMiddleware(thunk)
);

export default store;
```

## index.tsxの修正
　ReduxとReactを連携させます。

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

## Containerの作成
　ReduxのStoreとView(App.tsx)を紐付けるContainerを作成します。ContainerはView単位に作成していきます。面倒ですが，これもテストをしやすくするためです。

```js
/**
 * StateとPropertyをBindするよ
 */
const mapStateToProps = (state: AppState, props: AppProperty) => {
  return {
    message: state.advice.message,
  };
};

/**
 * ActionとPropertyをBindするよ
 */
const mapDispatchToProps = (dispatch: any, props: AppProperty) => {
  return {
    fetchAdvice: () => {
      dispatch(fetchAdviceAsync());
    },
  };
};

const AppContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

export default AppContainer;
```

## App.tsxを微修正
　ContainerとAppはProperty経由でやり取りするので，AppのProperty定義(interface)をそれに合わせて修正します。

```js
/**
 * Containerと連携するよ
 */
interface AppProperty {
  message?: string;
  fetchAdvice?: () => void;
}

interface AppState {}

/**
 * 生まれ変わったAppコンポーネント
 */
export default class App<AppState, AppProperty> extends React.Component {
  render() {
    return (
      <div className="App">
        <h1>{this.props.message}</h1>
        <button onClick="{this.props.fetchAdvice}">押せ！力の限り</push>
      </div>
    )
  }
}
```

　各属性をOptional型(nullかもしれない)にしておかないとContainerでエラーが出ます。

```js
const AppContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(App);
```

　ここでPropertyに何も渡さずにAppを使うからです。

## index.tsxを微修正
　Containerを作ったことでAppを直接指定する必要がなくなりました。

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

## 一旦動かしてみる
　アプリ自体はこれで完成です。

```bash
npm run start
```

　一旦動かしてみましょう。

## ユニットテストを書く
　非同期Actionのテストコードを書くところでハマったので，その部分だけ補足的に書いておきます。テスト対象は↓のこいつ。

```js
//非同期処理を行うAction
export const fetchAdviceAsync = () => {
  return async (dispatch: Dispatch<Action>, getState: () => AppState) => {
    try {
      const response = await axios.get('https://api.adviceslip.com/advice');
      const result: AdviceInfo = {
        message: response.data.slip.advice,
      };
      dispatch(fetchAdvice(result));
    } catch {
      //勇者よ，忘れず例外処理をやるのです
      //例外通知用の同期Actionを作るのもオススメです
    }
  };
};
```

## redux-mock-storeの導入
　ActionはStoreに依存しているため，依存関係を排除したプレーンなテストを実施したいところです。そのためにredux-mock-storeを使います。

```bash
npm i -D redux-mock-store
```

## Actionのテストコードを実装
　redux-mock-storeを使ってモック用Storeを作成します。その際に**Dispatchの型をredux thunk準拠(ThunkDispatch)にしておくことが重要です。この一手間を省くと型エラーが発生してActionを呼べなくなります**。

```js
describe('AdviceAction', () => {
  it('非同期処理', async () => {
    //この一手間が明暗を分ける
    type DispatchExts = ThunkDispatch<AppState, void, AnyAction>;
    const mockStore = configureStore<AppState, DispatchExts>([thunk]);
    const store = mockStore();
    await store.dispatch(fetchAdviceAsync());
    expect(store.getActions()[0]).toEqual({
      payload: {
        message: 'mocked',
      },
      type: 'FETCH_ADVICE',
    });
  });
});
```

## オマケ: Axiosのモック
　jestを使えばAxiosのモックも出来るので，よりベターです。

```js
jest.mock('axios');
import axios from 'axios';
(axios.get as any).mockResolvedValue({ data: { slip: { advice: 'mocked' } } });

describe('AdviceAction', () => {
  it('非同期処理', async () => {
    //以下略
```