はじめに
この記事は、redux-sagaのtake、takeEvery、takeLatestそれぞれの挙動を実際に動かして調べたので、備忘を兼ねてまとめたもの。
サンプルで使用したredux-sagaのversionは 1.0.5 です。
この記事に出てくるactionはreduxのactionのことを指しています。
9/29 追記:
サンプル公開しました。こちらからどうぞ。
TL;DR
- take: actionがdispatchされるのを待ち受ける。actionがdispatchされたら処理が進む。
- takeEvery: Actionがdispatchされるたびにredux-sagaのタスクが起動する。
- takeLatest: Actionがdispatchされるたびにredux-sagaのタスクが起動する。すでに同じActionによって起動したタスクがまだ終了していない場合は、そのタスクはキャンセルされる。
事の発端
React+redux+redux-sagaでアプリを開発中に、以下の様なtakeの使い方でactionを待ち受けていた。
function* sagaFunc() {
while(true) {
const action = yield take("SOME_ACTION")
const { result, error } = yield call(
// バックエンドサーバのAPI呼び出しなど時間のかかる処理の実行
)
// 以下省略
}
}
しかし、この様な使い方ではcallで呼び出した処理が終了するのを待っている間に、新たに SOME_ACTION
がdispatchされた場合、takeで待ち受けることができず、結果的にactionを取りこぼしてしまう。その対策として、takeEvery、takeLatestを使用することにした。
動作の確認に使用したコード
React+redux+redux-sagaで単純なアプリを用意した。画面にボタンが3個あり、それぞれクリックするとtake、takeEvery、takeLatestで待ち受けているactionが1000ミリ秒間隔で3回dispatchされる。
// 省略
const mapDispatchToProps = (dispatch) => ({
onClickTakeButton: () => {
let count = 0
const interval = setInterval(() => {
dispatch(takeSampleStart(count))
count++
if(count >= 3) {
clearInterval(interval)
}
}, 1000)
},
// 省略
以下が今回作成したsagaである。callで呼び出す時間がかかる処理として、擬似的に5000ミリ秒スリープするだけの関数(sleepAsync)を使用している。
import { take, takeEvery, takeLatest, call, put } from 'redux-saga/effects'
import {
TAKE_SAMPLE_START,
TAKE_EVERY_SAMPLE_START,
TAKE_LATEST_SAMPLE_START,
takeSampleSuccess,
takeEverySampleSuccess,
takeLatestSampleSuccess,
} from '../action/sampleAction'
function* handleTakeSampleStart() {
while(true) {
console.log('ready take action: TAKE_SAMPLE_START')
const action = yield take(TAKE_SAMPLE_START)
console.log(`take action ${JSON.stringify(action)}`)
console.log(`start call ${action.payload.count}`)
yield call(sleepAsync)
console.log(`finish call ${action.payload.count}`)
yield put(takeSampleSuccess())
}
}
function* handleTakeEverySampleStart() {
yield takeEvery(TAKE_EVERY_SAMPLE_START, runTakeEverySampleStart)
}
function* runTakeEverySampleStart(action) {
console.log(`take action ${action}`)
console.log(`start call ${action.payload.count}`)
yield call(sleepAsync)
console.log(`finish call ${action.payload.count}`)
yield put(takeEverySampleSuccess())
}
function* handleTakeLatestSampleStart() {
yield takeLatest(TAKE_LATEST_SAMPLE_START, runTakeLatestSampleStart)
}
function* runTakeLatestSampleStart(action) {
console.log(`take action ${action}`)
console.log(`start call ${action.payload.count}`)
yield call(sleepAsync)
console.log(`finish call ${action.payload.count}`)
yield put(takeLatestSampleSuccess())
}
const sleepAsync = async () => {
console.log('start sleepAsync')
await new Promise(r => setTimeout(r, 5000))
console.log('finish sleepAsync')
}
export default [
handleTakeSampleStart,
handleTakeEverySampleStart,
handleTakeLatestSampleStart,
]
import { fork } from 'redux-saga/effects'
import sampleSaga from './sampleSaga'
export default function* rootSaga() {
let sagas = []
sagas = sagas.concat(sampleSaga)
for (let i = 0; i < sagas.length; i++) {
yield fork(sagas[i]);
}
}
actionがdispatchされるたびにreducerでログ出力をしている。
// 省略
export default (state = initialState, action) => {
console.log(`dispatch action: ${JSON.stringify(action)}`)
switch(action.type) {
case TAKE_SAMPLE_SUCCESS:
// 省略
動作結果
それぞれのボタンを1回押した際のログ出力を確認する。
take
1回目のactionがdispatchされた時、takeして sleepAsync
の呼び出しまで実行している。
しかし、 sleepAsync
の終了を待っている間にdispatchされた2回目以降のactionはtakeできていない。
takeEvery
actionがdispatchされるたびにtakeし、 sleepAsync
が呼び出されている。
いずれも sleepAsync
の終了を待って、次のactionである TAKE_EVERY_SAMPLE_SUCCESS
をdispatchできている。
takeLatest
こちらもtakeEveryと同様にactionがdispatchされるたびにtakeし、 sleepAsync
が呼び出されている。
ただし、1回目、2回目の sleepAsync
を呼び出したタスクは、takeLatestの仕様によりキャンセルされている。(finish call 0と1のログがなく、finish call 2だけ出力されていることから判断できる。)
そのため、 sleepAsync
の呼び出しが終了した後にdispatchする TAKE_LATEST_SAMPLE_SUCCESS
も最後の1回のタスクからだけdispatchされている。
まとめ
redux-sagaでactionの取りこぼし対策としてtakeEveryとtakeLatestが有用そうだ。
両者の使い分けについては機会があれば別の記事にまとめたい。