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

## はじめに
この記事は、[redux-saga](https://redux-saga.js.org/)のtake、takeEvery、takeLatestそれぞれの挙動を実際に動かして調べたので、備忘を兼ねてまとめたもの。
サンプルで使用したredux-sagaのversionは 1.0.5 です。

この記事に出てくるactionは[reduxのaction](https://redux.js.org/basics/actions)のことを指しています。

9/29 追記:
サンプル公開しました。[こちら](https://github.com/r-nouchi/redux-saga-example)からどうぞ。

## TL;DR
- take: actionがdispatchされるのを待ち受ける。actionがdispatchされたら処理が進む。
- takeEvery: Actionがdispatchされるたびにredux-sagaのタスクが起動する。
- takeLatest: Actionがdispatchされるたびにredux-sagaのタスクが起動する。すでに同じActionによって起動したタスクがまだ終了していない場合は、そのタスクはキャンセルされる。

## 事の発端
React+redux+redux-sagaでアプリを開発中に、以下の様なtakeの使い方でactionを待ち受けていた。

```javascript
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/35310f6f-8386-4ce7-c1f4-94c528d843eb.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/d981c7e2-7bac-3cf1-7b35-2a89dab7b810.png)

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


### takeEvery
![スクリーンショット 2019-08-25 16.48.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/e55f3eb3-21c3-3fab-439d-44c1df7ee8cb.png)

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

### takeLatest
![スクリーンショット 2019-08-25 16.49.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/17e398f3-fdc5-7dff-65a2-a8fcf401ed32.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が有用そうだ。
両者の使い分けについては機会があれば別の記事にまとめたい。