Redux saga debounce and not just delay/cancel Redux saga debounce and not just delay/cancel reactjs reactjs

Redux saga debounce and not just delay/cancel


Redux saga now has a debounce function/effect:

import { call, put, debounce } from `redux-saga/effects`function* fetchAutocomplete(action) {  const autocompleteProposals = yield call(Api.fetchAutocomplete, action.text)  yield put({type: 'FETCHED_AUTOCOMPLETE_PROPOSALS', proposals: autocompleteProposals})}function* debounceAutocomplete() {  yield debounce(1000, 'FETCH_AUTOCOMPLETE', fetchAutocomplete)}


I was about to write an example using an array as a queue to store the actions to buffer, along with a setTimeout to flush the queue calling call() on each of them (and then cancelling the timeout accordingly if a new action comes in before it expires), but I noticed that now redux-saga supports Channels:

https://yelouafi.github.io/redux-saga/docs/advanced/Channels.html

They also have a built-in buffer to store actions while the saga is busy. Here the trick is to replace the api call from the docs example with your delay function, so that the saga is "busy" and will buffer actions for you.

const deferTime = 2000;const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));export function* sendClickEvent (event) {  yield put(action(event));}export function* clickSaga () {  // Create a channel (buffered by default)  const requestChan = yield actionChannel(WIDGET_CLICKED)  while (true) {    // Note: we now take actions from the channel (buffered)    const action = yield take(requestChan)     const state = yield select();    const debounce = action.meta && action.meta.debounce;    const payload = getWidgetClickPayload(state, action);    const defaultData = getDefaultData(state);    const event = {      name: payload.name,      data: Object.assign(defaultData, payload.data)    };    // This should "suspends" the saga and makes it buffer events.    yield call(delay, deferTime)    yield fork(sendClickEvent, event);  }}

You also have different buffer strategies to choose from.

Please note I'm not 100% sure my example will work in your case as I never used channels before, but hopefully you can adapt it to your problem.


If you schedule tasks individually for execution, they will fire all after a debounce period, however they wont be bundled in the same event loop; instead each delay call will schedule its execution in its own loop. If I'm not mistaken, what you want is to fire the grouped tasks in the same event loop after same delay.

The channel API doesn't offer actually a non-blocking take (and I think your case above suggests we should add it to the library). But you can implement a similar solution without much difficulty.

A possible solution is to split the work into 2 daemon sagas: the 1st will continually watch for actions and put debounced tasks in a shared queue. The 2nd will continually: 1. sleep for some time, 2. wake up and forks tasks for all queued actions until the queue is empty, then sleep again.

For example

import { delay } from 'redux-saga'import { take, put, call, fork, select } from 'redux-saga/effects'const deferTime = 2000;function* clickSaga () {  const taskQueue = []  // fork the worker tasks  yield fork(worker, taskQueue)  while (true) {    const action = yield take(WIDGET_CLICKED);    const state = yield select();    const debounce = action.meta && action.meta.debounce;    const payload = getWidgetClickPayload(state, action);    const defaultData = getDefaultData(state);    const event = {      name: payload.name,      data: Object.assign(defaultData, payload.data)    };    if(debounce) {      // debounce? batch execution      taskQueue.push({ task: sendClickEvent, event});    } else {      // no debounce, execute right now      yield fork(sendClickEvent, event)    }  }}function* worker(queue) {  while(true) {    // sleep    yield call(delay, deferTime)    // after wakeup, flush the batched tasks    let current    while(current = queue.shift()) {      const {task, event} = current      yield fork(task, event)    }  }}