admin管理员组

文章数量:1185210

Is there a way to debounce in Redux-Saga where subsequent calls are queued up behind the same delay, which keeps getting bumped by each new task added to queue. Similar to lodash's debounce .

I currently have something similar to redux-saga's debounce but removed the cancel part as I still want to action each task, I just want to bundle up all events to fire in a single thread later.

What I have currently:

const deferTime = 2000;
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

export function* sendClickEvent (event, debounce) {
  if (debounce) {
    yield call(delay, deferTime);
  }
  yield put(action(event));
}

export function* clickSaga () {
  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)
    };
    yield fork(sendClickEvent, event, debounce);
  }
}

I tried assigning fork to variable and then checking if that was running (.isRunning()) but didn't know how I could postpone that fork by another delay.

Is there a way to debounce in Redux-Saga where subsequent calls are queued up behind the same delay, which keeps getting bumped by each new task added to queue. Similar to lodash's debounce https://lodash.com/docs#debounce.

I currently have something similar to redux-saga's debounce but removed the cancel part as I still want to action each task, I just want to bundle up all events to fire in a single thread later.

What I have currently:

const deferTime = 2000;
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

export function* sendClickEvent (event, debounce) {
  if (debounce) {
    yield call(delay, deferTime);
  }
  yield put(action(event));
}

export function* clickSaga () {
  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)
    };
    yield fork(sendClickEvent, event, debounce);
  }
}

I tried assigning fork to variable and then checking if that was running (.isRunning()) but didn't know how I could postpone that fork by another delay.

Share Improve this question asked May 23, 2016 at 13:15 LabithiotisLabithiotis 4,1297 gold badges31 silver badges47 bronze badges
Add a comment  | 

6 Answers 6

Reset to default 16

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)
    }
  }
}

I don't know what is your application set up, but here is how I made debounce in my project:

I have root saga. It is all takeLatest

const productsSaga = function*() {
  yield all([
    takeLatest(SET_SEARCH_TERM, debounceAutocomplete), // <= type text in <input /> 
    takeLatest(GET_PRODUCTS, getProductsSaga), // <= request for products
    ... many more effects here
  ]);
};

debounceAutocomplete. I only used delay. That solved the issue

const debounceAutocomplete = function*() {
  yield delay(300); // <= here you debounce <input/> typing
  yield put({type: GET_PRODUCTS}); // <= here you takeLatest from <input/>
};

getProductsSaga which makes request

const getScientificReviewersSaga = function*() {
  yield put(toggleProductsLoading(true));
  const productsCategoryId = yield select(state => state.category.id);
  const state = yield select(selectProductsState);
  const data = {
    page: state.page,
    size: state.pageSize,
    productName: state.productName,
    productColor: state.productColor
  };
  const params = stringify(data);
  yield put({
    types: GET_PRODUCTS_TYPES,
    payload: {
      request: {
        url: `/${API.products}/${productsCategoryId}/products?${params}`,
        method: 'GET'
      }
    }
  } as Actions);
};

I also need setSearchTerm action creator to update state with search input before request and debounce:

const setSearchTerm = (name, value) => ({
  type: SET_SEARCH_TERM,
  payload: {
    name: name,
    value: value
  }
});

So in my component I dispatch this:

import { setSearchTerm } from '../Store/actions-sagas';
import { useDispatch } from 'react-redux';

const Component = () => {
  const dispatch = useDispatch();
  const updateSearchValues = (key, value) => dispatch(setSearchTerm(key, value));
}

You could use takeLatest with delay using both, should work

I have implemented this helper effect just like others(takeLatest, takeEvery). It will aggregate the actions dispatched with in 300ms and call the intended saga.

This will be helpful for the scenarios where you get actions with in a sort span of time where calling it in batch manner is expected.

Hoping this will be helpful for someone. This is similar to debounce implementation. This can be improved to debounced queue as well

export const watchAndAggregate = (pattern, saga, payloadAggregator, ...args) =>
    fork(function*() {
        while (true) {
            const action = yield take(pattern);
            let { payload } = action;

            while (true) {
                const { debounced, _action } = yield race({
                    debounced: delay(300),
                    _action: take(pattern),
                });

                if (debounced) {
                    yield call(saga, ...args.concat({ ...action, payload }));
                    break;
                }

               payload = payloadAggregator(_action);
            }
        }
    });

本文标签: javascriptRedux saga debounce and not just delaycancelStack Overflow