LoginSignup
14
10

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-07-04

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

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

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

Reduxの導入


sudo npm install redux react-redux --save

ActionCreator

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

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
    }
}
crud-front/src/Reducers/reducers.js

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

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

export default combineReducers({
    products: productsReducer,

});

index.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

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

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


sudo npm install redux-saga --save

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

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を組み込む

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')
)

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

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}
    },
}
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()を呼んでやります。

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>
            );
        }
    }
}
// -- いろいろ省略 --
14
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
10