reduxで非同期処理をするいくつかの方法（redux-thunk、redux-saga）

# 概要

`redux`の非同期処理のやり方を調べたので、まとめるため以下のような非同期で動くカウンターを作ります。

![counter.gif](https://qiita-image-store.s3.amazonaws.com/0/159260/9720117f-49cd-a0a4-0b5b-78b60000da1a.gif)

`redux`で非同期処理をどうするかというのは、結局なにに非同期処理を押し付けるかという問題になります。現在の主な押し付け先の選択肢は、以下の３種類のようです。

- コンポーネント
- Action (`redux-thunk`)
- Saga (`redux-saga`)

それぞれのやり方でカウンター書いてみます。

今回は、以下のようなAPIを使ってカウンターを作ることにします。現在のカウンターの値を受け取って、一秒後に+1した値を`resolve`しています。カウンターの`button`が押されると、何らかの方法で以下の非同期なAPIを呼んで、現在の`state`から次の`state`を計算します。また、今回の例ではこのAPIはつねにうまく動くとし、エラー処理は考えないことにします。

```API.js
export const nextCountAPI = currentCount => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(currentCount + 1)
    }, 1000)
  })
}
```

# コンポーネントに押し付ける

`middleware`を何も使わず普通に書くと、非同期処理はコンポーネント中のイベントハンドラで行うようになると思います。

## Promise

以下、コードです。

```text:index.js
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)
```

```text:Counter.js
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`を使って書き換えてあげると少しは見通しがよくなるかもしれません。

```diff:Counter.js
-  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`ではなく、関数になります。この関数中で、一つ前の例でイベントハンドラ中で行っていた処理をそのまま行うような感じです。

コードは以下です。

```diff:index.js
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)
```

```diff:Counter.js
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`のコードを見るとわかるように、コンポーネントは具体的な非同期処理の内容について一切気にせず、単に１つ`action`を`dispatch`するだけになっていてわかりやすいです。一方、デメリットとしては、`action`を関数にするというやり方自体が`redux`のやり方から大きく外れてしまっていて気持ち悪いことや、テストがしづらいことがあると思います。

# `redux-saga`

`redux-saga`では、非同期処理をSagaという独立したプロセスに押し付けます。`redux-thunk`と比べた場合のメリットとしては、非同期処理を行う`action`もプレーンな`Object`のまま保てることや、テストがしやすいことが挙げられます。また、複雑なアプリになるほど`redux-saga`の力が発揮されるっぽいです。デメリットは、めんどくさいことだと思います。

まず、以下のようにSagaを定義するファイルを書きます。

```text:sagas.js
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`のときと同じです。

```diff:index.js
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`が書きやすかったです。ただ、単純なアプリなら非同期処理はすべてコンポーネントに押し付けるのがシンプルで良い感じがしましたが、どうなんでしょうか。

以上、よろしくお願いします。