ソースコード
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
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
// 中味はまだ空
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
}
}
// Reducer達を一つに纏める
// 今回は必要ないけど。。
import {combineReducers} from 'redux'
import productsReducer from './productsReducer'
export default combineReducers({
products: productsReducer,
});
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
// -- 省略 --
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処理を例にとって書いていきます。
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)];
パッとみてもよくわからないと思うので順に見ていきます。
takeEveryの第一引数で指定されたアクションがどこかで呼ばれる
そのアクションの完了を待って第二引数が呼ばれる
yield call()で中の関数が実行され、Promiseオブジェクトが帰ってくるまで待つ
yield putで新たにActionをdispatchする
ざっくりこのような流れで非同期処理を実現しています。
function* などを見て戸惑った方は以下の記事がわかりやすいです。
https://qiita.com/kura07/items/cf168a7ea20e8c2554c6
https://qiita.com/kura07/items/d1a57ea64ef5c3de8528
Redux-Sagaを組み込む
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')
)
呼び出し用、成功した処理用、エラー処理用と分けてやる必要があります。(エラー分は今回は省略)
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}
},
}
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()を呼んでやります。
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>
);
}
}
}
// -- いろいろ省略 --