Redux Saga - Unable to cancel task

Viewed 365

I have a react project set up to work with redux saga, but for some reason, I'm unable to cancel a running saga / action task. The expectancy is that, after some action, (like user navigating away, or clicking on a button), the running saga would be cancelled. Tried catching the cancelled action, but doesn't happen after the saga has run completely:

function* fetchOverviewSaga(): SagaReturnType<any> {
  try {
    yield delay(5000);
    
    console.log('still running');

    const response = yield call(getOverviewData);

    console.log('still running');

    yield all([
        put(updateTagsPendingState(false)),
        put(updateTagsDataState(response.items)),
      ]);

  } finally {
    if(yield cancelled()) {
      console.log('saga task canceled');
    }
  }
}

function* cancelOverviewSaga(): SagaReturnType<any> {
  const runningAction = yield fork(fetchOverviewSaga);

  yield cancel(runningAction);
}

function* overviewSaga() {
  yield all([
    takeLatest(startOverview, fetchOverviewSaga),
    takeLatest(cancelOverview, cancelOverviewSaga)
  ]);
}

The result is that, even after the action was dispatched for cancelling (cancelOverviewSaga), the fetchOverviewSaga still runs, do gets catched in the if(yield cancelled()) only after it completely finished running. Not sure if this is the actual behaviour, would have expected to cancel when requested. Any ideas are most welcomed.

*Edit:

Upon calling the cancel action, the fetchOverviewSaga seems to be canceled, since it does log saga task canceled, however the ones remaining to run is the block from yield all([..])), looking at the console, probably the problem lies within that block

*Edit2:

To better illustrate the behaviour:

  • dispatch startOverview action
  • immediately cancel it
  • the log: saga task canceled
  • after 5000ms (the delay finishes in fetchOverviewSaga)
  • the log: 'still running' x2 and dispatches the yield all from fetchOverviewSaga
2 Answers

The same saga can run in multiple instances independently. So e.g. if you do

const task1 = yield fork(mySaga);
const task2 = yield fork(mySaga);
task1 === task2 // false

it will run two independent instances of mySaga, each with its own task. If you cancel one, it doesn't cancel the other one. So forking and immediately canceling your fetchOverviewSaga saga in cancelOverviewSaga will have no effect on the saga that is running as a result of dispatching the startOverview action.

In this case you can instead use e.g. the race effect to achieve your goal:

function* fetchOverviewSaga() {
  try {
    // your fetching logic ...
  } finally {
    if (yield cancelled()) {
      console.log("saga task canceled");
    }
  }
}

function* startOverviewSaga() {
  yield race([
    call(fetchOverviewSaga),
    take(cancelOverview),
  ]);
}

function* overviewSaga() {
  yield takeLatest(startOverview, startOverviewSaga);
}

The race effect waits for one if the items in the array to finish and then it cancels all other ones, and so:

  • If the cancelOverview action is disptached before the fetchOverviewSaga is finished it will cancel the fetchOverviewSaga
  • If the fetchOverviewSaga finishes before a cancel action is dispatched it will just stop waiting for the cancel action.

Working demo: https://codesandbox.io/s/https-stackoverflow-com-questions-69717869-redux-saga-unable-to-cancel-task-vu4b0?file=/src/index.js

When you cancel a saga, all sub tasks cancel. Example:

function* mainTask(){
  const task = yield fork(subTask1) // or call
  yield cancel(task)
}

function* subTask1(){
  yield fork(subTask2) // or call
}

function* subTask2(){
  ...
}

In this example, since cancel propagates downward, subTask1 will be cancelled through mainTask and subTask2 will be cancelled through subTask1.

However, when you yield put, you dispatched an action, which is caught in a reducer or listened in a saga. This is just like sending an erroneous e-mail, and sending a second e-mail to fix the error. Therefore, you can dispatch another action inside if (yield cancelled()) to undo what other actions do.

One way

  } finally {
    if(yield cancelled()) {
      yield all[
       put(cancelUpdateTagsPendingState()),
       put(cancelUpdateTagsDataState()),
      ]
    }
  }

Helpful docs

Related