概要
redux
の非同期処理のやり方を調べたので、まとめるため以下のような非同期で動くカウンターを作ります。
redux
で非同期処理をどうするかというのは、結局なにに非同期処理を押し付けるかという問題になります。現在の主な押し付け先の選択肢は、以下の3種類のようです。
- コンポーネント
- Action (
redux-thunk
) - Saga (
redux-saga
)
それぞれのやり方でカウンター書いてみます。
今回は、以下のようなAPIを使ってカウンターを作ることにします。現在のカウンターの値を受け取って、一秒後に+1した値をresolve
しています。カウンターのbutton
が押されると、何らかの方法で以下の非同期なAPIを呼んで、現在のstate
から次のstate
を計算します。また、今回の例ではこのAPIはつねにうまく動くとし、エラー処理は考えないことにします。
export const nextCountAPI = currentCount => {
return new Promise(resolve => {
setTimeout(() => {
resolve(currentCount + 1)
}, 1000)
})
}
コンポーネントに押し付ける
middleware
を何も使わず普通に書くと、非同期処理はコンポーネント中のイベントハンドラで行うようになると思います。
Promise
以下、コードです。
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import Counter from './Counter'
const initialState = {
count: 0,
isFetching: false,
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'REQUEST_NEXT_COUNT':
return {
...state,
isFetching: true,
}
case 'RECEIVE_NEXT_COUNT':
return {
...state,
count: action.nextCount,
isFetching: false,
}
default:
return state
}
}
export const requestNextCount = () => ({
type: 'REQUEST_NEXT_COUNT',
})
export const receiveNextCount = nextCount => ({
type: 'RECEIVE_NEXT_COUNT',
nextCount,
})
const store = createStore(reducer)
const render = () => {
ReactDOM.render(
<Counter store={store} />,
document.getElementById('root')
)
}
render()
store.subscribe(render)
import React from 'react';
import { requestNextCount, receiveNextCount } from './index'
import { nextCountAPI } from './API'
const Counter = ({ store }) => {
const { count, isFetching } = store.getState()
const handleNext = () => {
store.dispatch(requestNextCount())
nextCountAPI(count).then(nextCount => {
store.dispatch(receiveNextCount(nextCount))
})
}
return (
<div>
<h1>{isFetching ? '...' : count}</h1>
<button onClick={handleNext}>next</button>
</div>
)
}
export default Counter
イベントハンドラ中で非同期APIを呼んでいます。この書き方のメリットは、とにかくシンプルでわかりやすいことです。何も難しいことをしておらず、誰が見ても何をしているのかわかります。デメリットとしては、複雑なアプリになってくるとイベントハンドラがごちゃごちゃしてきて見通しが悪くなることや、コードが冗長になりやすいことが挙げられるかと思います。
async / await
イベントハンドラを以下のようにasync/await
を使って書き換えてあげると少しは見通しがよくなるかもしれません。
- const handleNext = () => {
+ const handleNext = async () => {
store.dispatch(requestNextCount())
- nextCountAPI(count).then(nextCount => {
- store.dispatch(receiveNextCount(nextCount))
- })
+ const nextCount = await nextCountAPI(count)
+ store.dispatch(receiveNextCount(nextCount))
}
redux-thunk
次に、middleware
のredux-thunk
を使います。redux-thunk
では非同期処理をaction
に押し付けます。押し付けられたaction
は、通常のaction
のようなプレーンなObject
ではなく、関数になります。この関数中で、一つ前の例でイベントハンドラ中で行っていた処理をそのまま行うような感じです。
コードは以下です。
import React from 'react'
import ReactDOM from 'react-dom'
-import { createStore } from 'redux'
+import { createStore, applyMiddleware } from 'redux'
+import thunk from 'redux-thunk'
import Counter from './Counter'
+import { nextCountAPI } from './API'
const initialState = {
count: 0,
isFetching: false,
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'REQUEST_NEXT_COUNT':
return {
...state,
isFetching: true,
}
case 'RECEIVE_NEXT_COUNT':
return {
...state,
count: action.nextCount,
isFetching: false,
}
default:
return state
}
}
export const requestNextCount = () => ({
type: 'REQUEST_NEXT_COUNT',
})
export const receiveNextCount = nextCount => ({
type: 'RECEIVE_NEXT_COUNT',
nextCount,
})
+export const nextCount = () => (dispatch, getState) => {
+ const { count } = getState()
+ dispatch(requestNextCount())
+ nextCountAPI(count).then(nextCount => {
+ dispatch(receiveNextCount(nextCount))
+ })
+}
-const store = createStore(reducer)
+const store = createStore(reducer, applyMiddleware(thunk))
const render = () => {
ReactDOM.render(
<Counter store={store} />,
document.getElementById('root')
)
}
render()
store.subscribe(render)
import React from 'react';
-import { requestNextCount, receiveNextCount } from './index'
+import { nextCount } from './index'
-import { nextCountAPI } from './API'
const Counter = ({ store }) => {
const { count, isFetching } = store.getState()
- const handleNext = () => {
- store.dispatch(requestNextCount())
- const nextCount = await nextCountAPI(count)
- store.dispatch(receiveNextCount(nextCount))
- }
+ const handleNext = () => {
+ store.dispatch(nextCount())
+ }
return (
<div>
<h1>{isFetching ? '...' : count}</h1>
<button onClick={handleNext}>next</button>
</div>
)
}
export default Counter
redux-thunk
のメリットとしては、コンポーネントから見たときに処理が単純になることだと思います。Counter.js
のコードを見るとわかるように、コンポーネントは具体的な非同期処理の内容について一切気にせず、単に1つaction
をdispatch
するだけになっていてわかりやすいです。一方、デメリットとしては、action
を関数にするというやり方自体がredux
のやり方から大きく外れてしまっていて気持ち悪いことや、テストがしづらいことがあると思います。
redux-saga
redux-saga
では、非同期処理をSagaという独立したプロセスに押し付けます。redux-thunk
と比べた場合のメリットとしては、非同期処理を行うaction
もプレーンなObject
のまま保てることや、テストがしやすいことが挙げられます。また、複雑なアプリになるほどredux-saga
の力が発揮されるっぽいです。デメリットは、めんどくさいことだと思います。
まず、以下のようにSagaを定義するファイルを書きます。
import { call, put, takeEvery } from 'redux-saga/effects'
import { requestNextCount, receiveNextCount } from './index'
import { nextCountAPI } from './API'
function* nextCount(action) {
yield put(requestNextCount())
const nextCount = yield call(nextCountAPI, action.count)
yield put(receiveNextCount(nextCount))
}
export default function* rootSaga() {
yield takeEvery('NEXT_COUNT', nextCount)
}
index.js
を以下のように書き換えます。Counter.js
はredux-thunk
のときと同じです。
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
-import thunk from 'redux-thunk'
+import createSaga from 'redux-saga'
import Counter from './Counter'
-import { nextCountAPI } from './API'
+import rootSaga from './sagas'
const initialState = {
count: 0,
isFetching: false,
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'REQUEST_NEXT_COUNT':
return {
...state,
isFetching: true,
}
case 'RECEIVE_NEXT_COUNT':
return {
...state,
count: action.nextCount,
isFetching: false,
}
default:
return state
}
}
export const requestNextCount = () => ({
type: 'REQUEST_NEXT_COUNT',
})
export const receiveNextCount = nextCount => ({
type: 'RECEIVE_NEXT_COUNT',
nextCount,
})
-export const nextCount = () => (dispatch, getState) => {
- const { count } = getState()
-
- dispatch(requestNextCount())
- nextCountAPI(count).then(nextCount => {
- dispatch(receiveNextCount(nextCount))
- })
-}
+export const nextCount = count => ({
+ type: 'NEXT_COUNT',
+ count,
+})
+const saga = createSaga()
-const store = createStore(reducer, applyMiddleware(thunk))
+const store = createStore(reducer, applyMiddleware(saga))
+saga.run(rootSaga)
const render = () => {
ReactDOM.render(
<Counter store={store} />,
document.getElementById('root')
)
}
render()
store.subscribe(render)
まとめ
思ったよりredux-saga
が書きやすかったです。ただ、単純なアプリなら非同期処理はすべてコンポーネントに押し付けるのがシンプルで良い感じがしましたが、どうなんでしょうか。
以上、よろしくお願いします。