Edited at

Reactでredux-sagaで非同期通信 〜redux-thunkとの比較も〜


Reactでの非同期通信処理の方法

Reduxのミドルウェアを使って実装します

※ Reactのコンポーネント内で処理する方法もありますが、コンポーネントと通信処理が密結合になってコンポーネントのソースが複雑になり、可読性が下がるなどの理由で採用されるケースはあまりないためこの方法はとりません


redux-sagaとredux-thunkの比較

非同期通信処理の代表である redux-sagaとredux-thunkについて比較してみます


redux-thunk

redux-thunk_arch.png

上図のように、redux-thunkを導入すると、action creatorの中に非同期通信処理を書いたり、action creatorからさらに別のaction creatorをcallすることができるようになります。

そのため、本来の reduxのアーキテクチャの形から逸脱することになり、記述の自由度が上がり開発者各々が書くことでソースコードの可読性が下がってしまいます。


redux-saga

redux-saga_arch.png

・Actionが発行→ DispatcherはStoreとSagaのスレッドにActionを渡す

・非同期処理をTaskとして登録しておく。このTaskはアプリ起動時からスレッドのように実行待ちになる

・Actionが渡されるとWatch TaskはそれをキャッチしTaskを実行。実行結果をAction Creatorに渡す

ポイントは Reduxとは独立して動く

また、テストも書きやすくなっています。

スクリーンショット 2019-07-28 19.34.12.png

【↑ Redux-Sagaのロゴ。Reduxのロゴと独立していることを示している】

redux-saga
redux-thunk

サイズ
14KB
352B

Githubスター数
18,450
12,778

記述量
多い
少ない

Action Creator
入れ子にならない
入れ子になるためコールバックが連なり可読性が下がる


redux-sagaの実装例

reduxの公式サンプルの counterを改造してボタンが押されてから1秒後にカウントを追加する非同期処理を実装していきます

参考

redux-saga official beginner tutorial

ソース

sagaを実行するために以下を実装します


  1. MiddleWareを作成

  2. 作成したMiddlewareを ReduxのStoreとconnectする

例)main.js

import React from 'react'

import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import Counter from './Counter'
import reducer from './reducers'
import { helloSaga } from './sagas'

// sagaMiddlewareを作成
const sagaMiddleware = createSagaMiddleware()
// SagaMiddlewareをStoreとconnect
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
// SagaMiddlewareを実行
sagaMiddleware.run(helloSaga)

const action = type => store.dispatch({type})
・・・

こちらが実行するSaga(helloSaga)。 SagaMiddleware実行時に引数に渡しています

saga.js

export function* helloSaga() {

console.log('Hello Sagas!')
}


非同期通信処理

クリックしてから1秒後に1カウントする処理を実行するボタンを追加

Counter.js

const Counter = ({ value, onIncrement, onDecrement, onIncrementAsync }) =>

<div>
<button onClick={onIncrementAsync}>
Increment after 1 second
</button>
{' '}
...
</div>

onIncrementAsync をStoreのactionにをconnectします

main.js

function render() {

ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() => action('INCREMENT')}
onDecrement={() => action('DECREMENT')}
onIncrementAsync={() => action('INCREMENT_ASYNC')} />,
document.getElementById('root')
)
}

非同期通信をするために、 Taskと TaskWatcherを定義します

import { put, takeEvery } from 'redux-saga/effects'

const delay = (ms) => new Promise(res => setTimeout(res, ms))

// ...

// worker Saga: 非同期のincrement taskを実行します
export function* incrementAsync() {
yield delay(1000) // Generatorをとめる働きをします。
yield put({ type: 'INCREMENT' })
}

// TaskWatcher: INCREMENT_ASYNCのたびにincrementAsync taskを新たに作成します
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}

actionのtypeが INCREMENT_ASYNC の場合に incrementAsync()を実行します


Effect API

put: Action Creatorを実行してActionをdispatchします

他にも、select, join, callなどがあります

参考:https://redux-saga.js.org/docs/api/


複数のSagaを同時に実行

sagas.js

export default function* rootSaga() {

yield all([
helloSaga(),
watchIncrementAsync()
])
}

上で定義した rootSagaをmiddleware実行時に引数に渡すことで

それぞれのsagaが平行に実行されます

main.js

// ...

import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = ...
sagaMiddleware.run(rootSaga)

// ...


まとめ

reduxのアーキテクチャーをそのままに独立したものとして非同期処理を実装でき、redux-thunkと違い書き方が統一されるため、redux-sagaで非同期処理を実装するのがいいと思います。


参考

redux-saga officai doc

redux-saga official github

redux-saga README_ja

redux-thunk github

りあクト!