Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

ソースコード
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>
            );
        }
    }
}
// -- いろいろ省略 --
tontoko
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away