redux-sagaで処理をタイムアウトする

redux上でいい感じに非同期処理を扱うredux-sagaですが、処理に時間制限を設けてそれを超えた場合はタイムアウトさせるなりなんなりしたい場合というのがありえます。

基本

ドキュメントによれば、以下のようにracedelayを組み合わせることで実現できるようです。

const {posts, timeout} = yield race({
  posts: call(fetchApi, '/posts'),
  timeout: call(delay, 1000)
})

この場合、処理fetchApiが成功した場合にはpostsのみに値が、1秒以内に同処理が終わらずタイムアウトした場合にはtimeoutのみに値(true)が、それぞれ入ることになります(成功しなかった処理の方はundefinedになる)。

応用

タイムアウト処理を多用する場合には、タイムアウト処理をcallなどで返ってくるオブジェクトに組み込んでしまうというアイデアがありえます。

例えば以下のようにすることでそのような対応が可能になります。

import {
  call as originalCall,
  race,
} from 'redux-saga/effects';
import { delay } from 'redux-saga';

function call(main, ...args) {
  const obj = originalCall(main, ...args);
  obj.withTimeout = time =>
    race({
      result: obj,
      isTimeout: call(delay, time),
    });
  return obj;
}

このとき、タイムアウト処理は以下のように記述できることになります。

const posts = yield call(fetchApi, '/posts').withTimeout(1000);

かなりスッキリしました。

処理に成功した場合にはposts.resultに値が、タイムアウトした場合にはposts.isTimeouttrueになっているはずです。

あるいは以下のようにしても可です。

const { result: posts, isTimeout: timeout } = yield call(fetchApi, '/posts').withTimeout(1000);

このようにすると、当初と同様にpostsあるいはtimeoutのいずれか一方のみに値が割り当てられることになりいます。

おまけ

コールバックを使うことでタイムアウト時の処理を指定できるようにしたのがこちら。

function call(main, ...args) {
  const obj = originalCall(main, ...args);
  obj.withTimeout = function (time, after) {
    switch (arguments.length) {
      case 2:
        return race({
          result: obj,
          isTimeout: delay(time).then(after),
        });
      default:
        return race({
          result: obj,
          isTimeout: call(delay, time),
        });
    }
  };
  return obj;
}

オーバーロードさせるための黒魔術のためもあってアロー関数が使えなくなった結果不格好になってしまったのが玉に瑕。

利用する場合は、たとえば以下のような感じ。

const { result: posts } = yield call(fetchApi, '/posts').withTimeout(1000, () => {
    throw new Error('timeout!');
  });

失敗時の条件分岐もスッキリしたんじゃないでしょうか。

参考