nullException
nullException

Reputation: 1122

Saga canceling debounced action

Dispatching a StopAction doesn't cancel the task. The successAction or errorAction are still getting dispatched even if a StopAction was called.

function* myTask(actionCreator, action) {
    try {
        const { cancelTask, response } = yield race({
            response: call(apiPromise, action.meta.id),
            cancelTask: take(StopAction.type)
        });
        if (cancelTask !== undefined) {
            return;
        }
        yield put(actionCreator.makeSuccessAction(response, action.meta));
    } catch (e) {
        yield put(actionCreator.makeErrorAction(e, action.meta));
    }
}

function* mySaga() {
    yield debounceFor(
        myActionCreator.loadAction.type,
        myTask,
        250,
        myActionCreator
    );
}

export function* debounceFor(pattern, saga, ms, ...args) {
    function* delayedSaga(action) {
        yield call(delay, ms);
        yield call(saga, ...args, action);
    }

    let task;
    while (true) {
        const action = yield take(pattern);
        if (task) {
            yield cancel(task);
        }

        task = yield fork(delayedSaga, action);
    }
}

Upvotes: 1

Views: 299

Answers (1)

Martin Kadlec
Martin Kadlec

Reputation: 4975

I think the problem is you are calling the Stop action during the "delay" phase (that is part of debounceFor implementation), during that phase the myTask saga hasn't started yet and therefore when the stop action happens there is nothing listening for it yet. After that the myTask saga finally starts but you are running the race effect after the stop action was already dispatched and so the race effect is finished with the api response instead.

The question is how to fix that. One option would be to merge the debounce/myTask sagas together so that you can wrap both the delay/api phase into single task and have the race effect cancel whichever it is at at the moment.

Another option would be to listen for the stop action two times, once in the delay phase and then again in the api call phases you already are. The implementation of the debounce could then look like this:

export function* debounceFor(pattern, cancelPattern, saga, ms) {
  function* delayedSaga(action) {
    if (cancelPattern) {
      let {cancelTask} = yield race({
        _: delay(ms),
        cancelTask: take(cancelPattern),
      });
      if (cancelTask) return;
    } else {
      yield delay(ms);
    }
    yield call(saga, action);
  }

  let task;
  while (true) {
    const action = yield take(pattern);
    if (task) {
      yield cancel(task);
    }
    task = yield fork(delayedSaga, action);
  }
}
// ....
yield debounceFor(
  myActionCreator.loadAction.type,
  StopAction.type, // added cancel type
  myTask,
  250,
  myActionCreator
);

I should also point out that latest redux-saga supports debounce natively: https://redux-saga.js.org/docs/api/#debouncems-pattern-saga-args

Though for more complex implementations (like this one probably) you will end up using custom implementation anyway.

Upvotes: 2

Related Questions