13
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

redux-sagaのtakeでactionを取りこぼさないようにtakeEveryとtakeLatestを使う

Last updated at Posted at 2019-08-25

はじめに

この記事は、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される。

スクリーンショット 2019-08-25 16.32.54.png

sampleContainer.js

// 省略

const mapDispatchToProps = (dispatch) => ({
  onClickTakeButton: () => {
    let count = 0
    const interval = setInterval(() => {
      dispatch(takeSampleStart(count))
      count++
      if(count >= 3) {
        clearInterval(interval)
      }
    }, 1000)
  },
 
// 省略

以下が今回作成したsagaである。callで呼び出す時間がかかる処理として、擬似的に5000ミリ秒スリープするだけの関数(sleepAsync)を使用している。

sampleSaga.js
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,
]

rootSaga.js
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でログ出力をしている。

sampleReducer.js

// 省略

export default (state = initialState, action) => {
  console.log(`dispatch action: ${JSON.stringify(action)}`)
  switch(action.type) {
    case TAKE_SAMPLE_SUCCESS:

// 省略

動作結果

それぞれのボタンを1回押した際のログ出力を確認する。

take

スクリーンショット 2019-08-25 16.43.15.png

1回目のactionがdispatchされた時、takeして sleepAsync の呼び出しまで実行している。
しかし、 sleepAsync の終了を待っている間にdispatchされた2回目以降のactionはtakeできていない。

takeEvery

スクリーンショット 2019-08-25 16.48.17.png

actionがdispatchされるたびにtakeし、 sleepAsync が呼び出されている。
いずれも sleepAsync の終了を待って、次のactionである TAKE_EVERY_SAMPLE_SUCCESS をdispatchできている。

takeLatest

スクリーンショット 2019-08-25 16.49.38.png

こちらも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が有用そうだ。
両者の使い分けについては機会があれば別の記事にまとめたい。

13
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?