redux上でいい感じに非同期処理を扱うredux-sagaですが、処理に時間制限を設けてそれを超えた場合はタイムアウトさせるなりなんなりしたい場合というのがありえます。
基本
ドキュメントによれば、以下のようにrace
とdelay
を組み合わせることで実現できるようです。
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.isTimeout
がtrue
になっているはずです。
あるいは以下のようにしても可です。
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!'); });
失敗時の条件分岐もスッキリしたんじゃないでしょうか。