React+ReduxにRedux-Sagaを導入して非同期処理をさせる手順

ソースコード
https://github.com/tontoko/react-redux-saga-rails

↓こちらの記事を参考にまずReact+Rails(API)の環境を整えました。

Ruby on Rails+ReactでCRUDを実装してみた
https://qiita.com/yoshimo123/items/9aa8dae1d40d523d7e5d

# Reduxの導入

```bash

sudo npm install redux react-redux --save
```

## ActionCreator

```js:crud-front/src/Actions/actions.js

export default {
    create: (data) => {
        return { type: 'CREATE', data }
    }, 
    update: (id, data) => {
        return { type: 'UPDATE', id, data }
    },
    delete: (id) => {
        return { type: 'DELETE', id }
    },
    init: () => {
        return { type: 'INIT' }
    },
}
```

## Reducer

```js:crud-front/src/Reducers/productsReducer.js

// 中味はまだ空

const initialState = {
    products: [],
    isFetching: false,
}

export default function productReducer(state = initialState, action) {
    switch (action.type) {
        case 'CREATE':
            return Object.assign({}, state, {
            })
        case 'UPDATE':
            return Object.assign({}, state, {
            })
        case 'DELETE':
            return Object.assign({}, state, {
            })
        case 'INIT':    
            return Object.assign({}, state, {
            })
        default:
            return state
    }
}
```

```js:crud-front/src/Reducers/reducers.js

// Reducer達を一つに纏める
// 今回は必要ないけど。。

import {combineReducers} from 'redux'
import productsReducer from './productsReducer'

export default combineReducers({
    products: productsReducer,
    
});
```

## index.js

```js:crud-front/src/index.js

// -- 省略 --
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import Reducers from './Reducers/reducers'

const store = createStore(
    Reducers,
)

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

## MainContainer.js

```js:crud-front/src/Components/MainContainer.js

// -- 省略 --
import { connect } from 'react-redux'
import Actions from '../Actions/actions'
import { bindActionCreators } from 'redux'

const mapStateToProps = state => {
    return state
}

const mapDispatchToProps = dispatch => {
    return {
        init: () => dispatch(Actions.init()),
        create: (data) => dispatch(Actions.create(data)),
        update: (id, product) => dispatch(Actions.update(id, product)),
        delete: (id) => dispatch(Actions.delete(id)),
    }
}

//// こっちでもいいかな
// const mapDispatchToProps = dispatch => {
//     return bindActionCreators(Actions, dispatch)
// }

export default connect(mapStateToProps, mapDispatchToProps)(MainContainer)
```

# 非同期処理をどこに書くべきか

Redux導入の雛形はできたものの、ここで問題が起きる。

reduxで非同期処理をするいくつかの方法（redux-thunk、redux-saga）
https://qiita.com/muiscript/items/63386fd65c7e9f06f5d4

Actionをプレーンに保てテストもしやすい、というRedux-Sagaを試してみます。

# Redux-Sagaの導入

Redux-Sagaの概要については以下の記事もわかりやすいです。

redux-sagaで非同期処理と戦う
https://qiita.com/kuy/items/716affc808ebb3e1e8ac

【React】 redux-saga でAPIを叩く
https://k-tomoo.hatenablog.com/entry/2018/03/12/151045

まずはインストールします。

```bash

sudo npm install redux-saga --save
```

Saga部分をInit処理を例にとって書いていきます。

```js:crud-front/src/Saga/Init.js

import axios from "axios"
import { put, call, takeEvery } from 'redux-saga/effects';

const initAjax = () => axios.get('http://localhost:3001/products')
    .then((res) => {
        const data = res.data
        console.log(data)
        return { data }
    })
    .catch((error) => {
        return { error }
    })

function* initProduct() {
    // 3.
    const { data, error } = yield call(initAjax);
    console.log(data)

    if (data) {
        // 4.
        yield put({ type: "INIT_SUCCEEDED", data });
    } else {
        // todo: エラーハンドリング
        // 今回はエラー処理は省きます
    }
}

// 1.& 2.
export default [takeEvery("INIT", initProduct)];
```

パッとみてもよくわからないと思うので順に見ていきます。

1. takeEveryの第一引数で指定されたアクションがどこかで呼ばれる

2. そのアクションの完了を待って第二引数が呼ばれる

3. yield call()で中の関数が実行され、Promiseオブジェクトが帰ってくるまで待つ

4. yield putで新たにActionをdispatchする

ざっくりこのような流れで非同期処理を実現しています。

function* などを見て戸惑った方は以下の記事がわかりやすいです。
https://qiita.com/kura07/items/cf168a7ea20e8c2554c6
https://qiita.com/kura07/items/d1a57ea64ef5c3de8528

## Redux-Sagaを組み込む

```js:crud-front/src/index.js

import createSagaMiddleware from 'redux-saga'
import { all } from 'redux-saga/effects'

import Init from './Saga/Init'
import Create from './Saga/Create'
import Update from './Saga/Update'
import Delete from './Saga/Delete'

// ここで一つにまとめます
function* rootSaga() {
    yield all([
        ...Init,
        ...Create,
        ...Update,
        ...Delete,
    ])
}

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
    Reducers,
    applyMiddleware(sagaMiddleware)
)

sagaMiddleware.run(rootSaga)

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

呼び出し用、成功した処理用、エラー処理用と分けてやる必要があります。(エラー分は今回は省略)

```js:crud-front/src/Actions/actions.js

export default {
    create: (data) => {
        return { type: 'CREATE', data }
    }, 
    createSuccess: (data) => {
        return { type: 'CREATE_SUCCEEDED', data }
    },
    update: (id, data) => {
        return { type: 'UPDATE', id, data }
    },
    updateSuccess: (id, data) => {
        return { type: 'UPDATE_SUCCEEDED', id, data }
    },
    delete: (id) => {
        return { type: 'DELETE', id }
    },
    deleteSuccess: (id) => {
        return { type: 'DELETE_SUCCEEDED', id }
    },
    init: () => {
        return { type: 'INIT' }
    },
    initSuccess: (data) => {
        return { type: "INIT_SUCCEEDED", data}
    },
}
```

```js:crud-front/src/Reducers/productsReducer.js

import axios from "axios"

const initialState = {
    products: [],
    isFetching: false,
}

export default function productReducer(state = initialState, action) {
    switch (action.type) {
        case 'CREATE':
            return Object.assign({}, state, {
                isFetching: true,
            })
        case 'CREATE_SUCCEEDED':
            return Object.assign({}, state, {
                products: [...state.products, action.data],
                isFetching: false,
            })
        case 'UPDATE':
            return Object.assign({}, state, {
                isFetching: true,
            })
        case 'UPDATE_SUCCEEDED':
            const updateIndex = state.products.findIndex(x => x.id === action.id)
            const updatedProductsState = state.products
            updatedProductsState.splice(updateIndex, 1, action.data)
            return Object.assign({}, state, {
                products: updatedProductsState,
                isFetching: false,
            })
        case 'DELETE':
            return Object.assign({}, state, {
                isFetching: true,
            })
        case 'DELETE_SUCCEEDED':
            const deleteIndex = state.products.findIndex(x => x.id === action.id)
            const deletedProductsState = state.products
            deletedProductsState.splice(deleteIndex, 1)
            return Object.assign({}, state, {
                products: deletedProductsState,
                isFetching: false,
            })
        case 'INIT':    
            return Object.assign({}, state, {
                products: [],
                isFetching: true,
            })
        case 'INIT_SUCCEEDED':
            return Object.assign({}, state, {
                products: action.data,
                isFetching: false,
            })
        default:
            return state
    }
}
```

ここまでで導入は完了です。
後は例としてinit()を呼んでやります。

```js:crud-front/src/Components/MainContainer.js

class MainContainer extends React.Component {
// -- いろいろ省略 --
    componentDidMount() {
        this.props.init()
    }

    render() {
        if (this.props.isFetching === true) {
            return (<div />)
        } else {
            return (
                <div className='app-main'>
                    <FormContainer createProduct={this.props.create} />
                    <ProductsContainer deleteProduct={this.props.delete} updateProduct={this.props.update} />
                </div>
            );
        }
    }
}
// -- いろいろ省略 --
```