Reactのアーキテクトを考える道のり - その3(非同期通信)

[Reduxを使ってコンポーネントの状態を更新できるようになった](https://qiita.com/motuo/items/761840cedb5db20e08f6)私は、早速、APIを使って取得した値をコンポーネントに反映させようとしました。

## サンプル画面イメージ
作ったサンプルは、3秒待機→https://api.github.com/zen からメッセージを取得して`component`に反映するだけの簡単なものです。

![sampleImage.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154110/549970be-5c9a-d834-9fbb-c900b2396785.gif)

## 非同期処理をコンポーネント内にかくと。。。

こんな感じでできます。

```javascript
import React, { useEffect } from 'react'
import { Typography, Grid, Paper, Link } from '@material-ui/core';
import { sampleSagaActions } from '../../containers/SagaSampleContainer';
import { SampleSagaState } from '../../modules/SampleSaga';
import axios from 'axios';

type props = sampleSagaActions & SampleSagaState;

async function dummyWait() {
    return await new Promise(resolve => {
        const duration = 3000;
        setTimeout(() => {
            resolve({ data: duration });
        }, duration);
    });
}
export const SampleSaga: React.FC<props> = (props) => {
    //ここに画面描画時のAPI取得処理を記述
    useEffect(() => {
        props.setMessage("loading...");
        const initialFunction = async () => {
            //あえて、数十秒止めてLoadingを文字表記
            await dummyWait();
            axios.get('https://api.github.com/zen')
                .then((res) => {
                    props.setMessage(res.data);
                })
        };
        initialFunction();
    }, [])
    return (
        <div>
            <Grid container justify='center' alignItems='center'>
                <Grid item>
                    < Typography variant='h3' color="secondary"> This is a sample page of redux-saga!</Typography >
                </Grid>
                <Grid item>
                    <Grid container alignItems='center'>
                        <Grid item>
                            <Typography variant='h4' style={{ padding: 10 }}>Message from GitHub API is</Typography>
                            <Paper style={{ backgroundColor: '#cfe8fc' }}>
                                <Typography variant='h6'>{props.message}</Typography>
                            </Paper>
                            <Typography variant='h4' style={{ padding: 10 }}>
                                This api calls <Link href='https://api.github.com/zen'>https://api.github.com/zen</Link>
                            </Typography>
                        </Grid>
                    </Grid>
                </Grid>
            </Grid>
        </div>
    )
}
```
もちろんこれでも動きます:+1:
。。。けど、このままだとコンポーネント単位でのテストをするときに辛くなりそうだし、せっかく`Redux`使ってコンポーネントからロジックを外に出したのに、ビジネスロジックが入り込みそうな気がします。

というわけで、一般的に使われている非同期処理の作り方を調査してみました。

## 非同期処理をReduxで実装するときによく使われているライブラリ
調査したところ、`Redux-thunk`と`Redux-Saga`が多く使われているようです。
私は比較した結果、`Redux-Saga`を採用することにしました。
`Redux-thunk`はActionに非同期処理を書けるようにする`middleware`ですが、個人的には`Action`はStoreを更新するきっかけを定義する役割に限定する方が、システムが大きくなっても、分かり易い設計を保てるのではないかと考えたためです。
ただ、一般的には`Redux-Saga`の方が`Redux-thunk`に比べて学習コストが高めだと言われる傾向もあるようですので、プロジェクトによっては`Redux-thunk`を使う選択肢もアリなのかもしれません。

## というわけで、Redux-Sagaを使うとこうなった
### コンポーネント

```javascript
import React, { useEffect } from 'react'
import { Typography, Grid, Paper, Link } from '@material-ui/core';
import { sampleSagaActions } from '../../containers/SagaSampleContainer';
import { SampleSagaState } from '../../modules/SampleSaga';

type props = sampleSagaActions & SampleSagaState;

export const SampleSaga: React.FC<props> = (props) => {
    //ここに画面描画時のAPI取得処理を記述
    useEffect(() => {
        props.getMessage()
    }, [])
    return (
        <div>
            <Grid container justify='center' alignItems='center'>
                <Grid item>
                    < Typography variant='h3' color="secondary"> This is a sample page of redux-saga!</Typography >
                </Grid>
                <Grid item>
                    <Grid container alignItems='center'>
                        <Grid item>
                            <Typography variant='h4' style={{ padding: 10 }}>Message from GitHub API is</Typography>
                            <Paper style={{ backgroundColor: '#cfe8fc' }}>
                                <Typography variant='h6'>{props.message}</Typography>
                            </Paper>
                            <Typography variant='h4' style={{ padding: 10 }}>
                                This api calls <Link href='https://api.github.com/zen'>https://api.github.com/zen</Link>
                            </Typography>
                        </Grid>
                    </Grid>
                </Grid>
            </Grid>
        </div>
    )
}
```

### saga

```javascript
import { put, takeEvery, call } from 'redux-saga/effects'
import { sampleSagaActions } from '../modules/SampleSaga';
import { getGithubApi } from '../API/github';

function dummyWait() {
    return new Promise(resolve => {
        const duration = 3000;
        setTimeout(() => {
            resolve({ data: duration });
        }, duration);
    });
}

function* getMessage() {
    yield put(sampleSagaActions.setMessage("Loading...(3 seconds wait by using setTimeout Function!)"));
    yield call(dummyWait);
    const message: string = yield call(getGithubApi);
    yield put(sampleSagaActions.setMessage(message));
}

// Watch actions
export function* watchApiAsync() {
    yield takeEvery('ACTIONS_GET_MESSAGE', getMessage);
}
```

### module(reducer)

```javascript

import { reducerWithInitialState } from 'typescript-fsa-reducers';
import actionCreatorFactory from 'typescript-fsa';

const actionCreator = actionCreatorFactory();

export interface SampleSagaState {
    message: string;
}

const initialState: SampleSagaState = {
    message: ''
};

export const sampleSagaActions = {
    getMessage: actionCreator<void>('ACTIONS_GET_MESSAGE'),
    setMessage: actionCreator<string>('ACTIONS_SET_MESSAGE')
};

export const sampleSagaReduecer = reducerWithInitialState(initialState)
    .case(sampleSagaActions.setMessage, (state, message) => {
        const newState = Object.assign({}, state);
        newState.message = message;
        return newState;
    })
    .default((state) => {
        return state
    }
    );
```


### GithubApiを呼び出す処理

```javascript
import axios from 'axios'
/**
 * 手抜きですが、API取得処理
 */
export const getGithubApi = async () => {
    const response = await axios.get('https://api.github.com/zen');
    return response.data;
}
```

このようにすることで、
1. コンポーネントはあくまでもActionを呼ぶだけ
2. 非同期処理はSagaで行う

という役割分担が分かり易くできた気がします。

## 非同期処理を調べる途中で参考にした記事など
1. [ReactのRedux非同期処理がサルでも分かる超解説](https://qiita.com/jabba/items/5ebea8d19809a4e0f8c6)
2. [reduxで非同期処理をするいくつかの方法（redux-thunk、redux-saga）](https://qiita.com/muiscript/items/63386fd65c7e9f06f5d4)
3. [redux-sagaで非同期処理と戦う](https://qiita.com/kuy/items/716affc808ebb3e1e8ac)