React + ReduxでRssリーダーもどきを作ってみる

# はじめに
[前にReact + Redux](https://minase-program.hatenablog.com/entry/2019/02/13/225531?_ga=2.15810431.89909843.1566171090-1961159496.1467993109)はやったことがあったけど、型を付けてなかったり、非同期処理を入れてなかったりするので、今回はその辺を踏まえてやってみる。

# TL;DR.
[ソースコード](https://github.com/Tetsuya-Minase/rss-reader-sample/tree/master/react)

# 事前準備
まずは`create-react-app`を使ってプロジェクトの雛形を作るとこから。
作れたら必要なライブラリをインストールする

``` sh
# プロジェクト作成
$ create-react-app react-sample --typescript
$ cd react-sample
# ライブラリインストール
# redux, react-reduxはreduxやるのに入れるいつもの
# redux-thunkはreduxで非同期処理をする際によしなにしてくれるライブラリ
$ npm install --save redux react-redux redux-thunk
# 必要に応じて型定義も
$ npm install --save-dev @types/redux @types/react-redux @types/redux-thunk
```

# 実装
## Action
よくあるActionと非同期用のActionを定義しておく。
ThunkActionに型が付いてないところがあるけど、`getState`と`extraArgument`の戻り値なので一旦付けないでおく。

``` typescript
import {Action, Dispatch} from 'redux';
import {RssReaderStates} from '../types/RssReaderStates';
import axios from 'axios';
import {ThunkAction} from 'redux-thunk';

// type定義
export enum RSS_READER_TYPES {
    ADD_LIST = 'ADD_RSS_LIST',
    FETCH = 'FETCH_RSS'
}

// アクション定義
interface IAddRssListAction extends Action{
    type: typeof RSS_READER_TYPES.ADD_LIST;
    payload: RssReaderStates[]
}

// ActionCreator群
// リストに追加用Action
export const addRssList = (rssList: RssReaderStates[]): IAddRssListAction => ({
    type: RSS_READER_TYPES.ADD_LIST,
    payload: [...rssList]
});
// Fetch用Action
export const fetchRss = (): ThunkAction<void, any, any, IAddRssListAction> => {
    return async (dispatch: Dispatch<IAddRssListAction>) => {
				// 今回取得先は`json-server`で返すJSONから取得
        const result = await axios.get<RssReaderStates[]>('http://localhost:4000/result');
        dispatch(addRssList(result.data));
    }
};

// Actionの型を返す
export type RssActions = (
    ReturnType<typeof addRssList>
    );
```

## Reducer

よくあるReducerのままだと思う。
今回はAction1つしか用意していないので、意味ないけど一応switchで書いておく。

```typescript
import {RssReaderStates} from '../types/RssReaderStates';
import {RSS_READER_TYPES, RssActions} from '../actions/RssReaderAction';

export const rssReaderReducer = (state: RssReaderStates[] | never = [], action:RssActions): RssReaderStates[] => {
    switch (action.type) {
        case RSS_READER_TYPES.ADD_LIST:
            return [...state, ...action.payload];
        default:
            return state;
    }
};

// =================
// 以降index.tsに記載
// =================
import {combineReducers} from 'redux';
import {rssReaderReducer} from './RssReaderReducer';

export default combineReducers({rssReaderReducer});
```

## Store
今回`ReduxThunk`を使用したので、middleWareを追加しておく。

```typescript
import ReduxThunk, {ThunkMiddleware} from 'redux-thunk';
import {applyMiddleware, createStore} from 'redux';
import rootReducer from '../reducers';
import {RssReaderStates} from '../types/RssReaderStates';
import {RssActions} from '../actions/RssReaderAction';

export default createStore(
    rootReducer,
    applyMiddleware(ReduxThunk as ThunkMiddleware<RssReaderStates, RssActions>)
);
```

## Container
非同期のリクエストをする用のボタ用の結果を表示する用のリストの2つを用意する。

### Button
```typescript
import React from 'react';
import {fetchRss, RssActions} from '../actions/RssReaderAction';
import {connect} from 'react-redux';
import {ThunkDispatch} from 'redux-thunk';
import {RssReaderStates} from '../types/RssReaderStates';

type DispatchProps = {
    onClick: () => void;
};
// Action発火用ボタン
class FetchButton extends React.Component<DispatchProps> {
		// 初期表示用に読み込まれたときにイベントを発火させる
    componentDidMount(): void {
        this.props.onClick();
    }
    render() {
        return (
            <button
                onClick={() => {
                    this.props.onClick();
                }}
            >
                追加
            </button>
        );
    }
};

// props返却用
const mapDispatchToProps = (
    dispatch: ThunkDispatch<RssReaderStates, undefined, RssActions>
): DispatchProps => ({
    onClick: () => {
        dispatch(fetchRss());
    },
});

export default connect(null, mapDispatchToProps)(FetchButton);
```

### List
受け取ったStateをPropsに変換して表示用のListに渡す。
正直、ここなくても良い気がするけど一旦噛ませない方法がわからなかったので入れておく。

```typescript
import React from 'react';
import {connect} from 'react-redux';
import {RssReaderStates} from '../types/RssReaderStates';
import ListComponent from '../components/ListComponent';
import {RssReaderProps} from '../types/RssReaderProps';

const stateToProps = (state: RssReaderStates[]): RssReaderProps => {
    return {rssList: state};
};
export default connect(stateToProps)(ListComponent);
```

## Component
表示用の処理群。表示する以外の役割を内容にする。
propsのrssListが何故か1段ずれて入ってくるので、対応している。(型内容は後述)
なんとかしたいけど、ぱっと解決できなかったのとサンプルだからという言い訳で放置する。
解決策ご存知の方教えて頂けると幸いです。

``` typescript
import React from 'react';
import {RssReaderStates} from '../types/RssReaderStates';
import {RssReaderProps} from '../types/RssReaderProps';

export default (props: RssReaderProps) => (
        <ul>
            {props.rssList['rssReaderReducer']
                .map((rss: RssReaderStates, index: number) =>
                    <li key={index}>
                        <a href={rss.url} target="_blank">{rss.title}</a>: {rss.description}
                    </li>)
            }
        </ul>
    );
```

## 型一覧
### RssReaderProps
props用の型。
ここで指定した型と何故かずれるので↑のような解決策をしている。

```typescript
import {RssReaderStates} from './RssReaderStates';

export type RssReaderProps = {
    rssList: RssReaderStates[]
}
```
### RssReaderState
Stateの型。

```typescript
export type RssReaderState = {
    title: string,
    description: string,
    url: string
};
```

# 実行結果
画像のような結果になればOK！
ちなみにボタンを押したところで同じ内容が追加されるだけになる。

# まとめ
今回はReact + ReduxでRssリーダーを作ってみた。
Reduxで非同期処理ができないことがびっくりだったり、型付けたらReact,Reduxの型が難解過ぎて苦労した。
今回はなんとか動くように作っただけなので、また何か作って行きたいところ。
それでは、今回はこの辺で。

# 参考サイト
- [React](https://ja.reactjs.org/)
- [Redux](https://redux.js.org/)
- [redux-thunk](https://github.com/reduxjs/redux-thunk)