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
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
I have the following problem: With react saga i create an action channel and listen "requested" actions then put them into the action channel. With fork I create workers (with amount of WORKER_COUNT) which listen on that channel. This workers do call a function which takes time to finish it. I want to take action out of queue, if user wants to cancel the action before it finishes. How can I fix that problem ?
Take it as example, I have 2 workers, and user dispatched 3 "requested" actions at same time, as for first 2 actions dispatched, doDownload function would be in progress, the last action would be waiting in queue and user cancels that last action dispatched, I need to take then that last action from the actionChannel so it will not be proceeded with one of the two workers
How can i fix that problem?
function* handleRequest(streamChannel: Channel<unknown>) {
while (true) {
const payload = (yield* take(streamChannel)) as requestPayload;
yield* call(doDownload, payload);
}
}
function* watchRequest() {
const chan = yield* call(channel);
for (var i = 0; i < WORKER_COUNT; i++) {
yield* fork(handleRequest, chan);
}
while (true) {
const {payload} = yield* take(requested);
yield* put(chan, payload);
}
}
I need to run a saga every time an action is performed - but also cancel it when a logout action is dispatched. I tried using the while pattern described in the documentation, but this is only firing on the first action dispatch (but it's cancelling as expected).
export default function* rootSaga() {
while (yield take(actionTypes.sagas.RUN_COURSES_SAGA)) {
const syncTask = yield fork(processCourseLibraryFiles)
yield take(actionTypes.auth.LOGOUT_USER)
yield cancel(syncTask)
}
}
I tried replacing this is takeEvery, which fixes the problem of it only firing once - but now I'm stuck on how to cancel each of these sagas on logout action.
export default function* rootSaga() {
yield takeEvery('actionName', fn)
// some way to cancel these sagas on action
}
Appreciate any insights!
I am dispatching an action let's say "GET_STATUS" in a loop for X number of time from a component.
In saga file I have
function* actionWatcher() {
yield all([
takeLatest(Actions.GET_LATEST, getLatest),
]);
}
Inside getLatest* function there is this API call
//Some code
const results = yield call(api, {params});
//code after
callback()
I can clearly see API being called X number of time in network and also in chrome debugger I can see //Some code is executed X number of time. But //code after is executed only once in the end and callback function is being called just once in the end.
I am expecting to be called for each occurrence.
If multiple Actions.GET_LATEST happen in rapid succession, then takeLatest is designed to cancel the old saga, and start a new one. If the saga is canceled while it's executing const results = yield call(api, {params});, that means it will never get to callback()
If you don't want them to be canceled, then use takeEvery instead of takeLatest
function* actionWatcher() {
yield all([
takeEvery(Actions.GET_LATEST, getLatest),
]);
}
If you want to keep the cancellation, but you need the callback to be called even if it's cancelled, you can use a try/finally:
function* getLatest() {
try {
const results = yield call(api, {params});
} finally {
// This code will run whether it completes successfully, or throws an error, or is cancelled
callback();
}
}
If you need to specifically check whether it was cancelled in order to perform custom logic, you can do so with an if (yield cancelled()) in the finally block:
function* getLatest() {
try {
const results = yield call(api, {params});
callback(); // This line will only run if it's not cancelled and does not throw
} finally {
if (yield cancelled()) {
// This line will only run if cancelled
}
}
}
I have the following scenario:
export function* addCircle(circleApi, { payload }) {
try {
const response = yield apply(
circleApi,
circleApi.addCircle,
[payload]
);
if (response.error_type) {
yield put(addCircleFailedAction(response.error));
} else {
yield put(addCircleSucceededAction(response));
}
} catch (err) {
console.error(err);
}
}
export function* addTender(tenderApi, { payload }) {
try {
// NOTE: I want this to finish before continuing with rest of saga below.
yield call(addCircleAction(payload.circlePayload));
// Rest of saga removed for brevity.
} catch (err) {
console.error(err);
}
}
So, basically addCircle is making an API call, and depending on its success I call the appropriate redux action. Now, inside another saga I call the action responsible for addCircle saga, and I want it to finish execution before I continue with the rest of the saga. I tried to use call, but it basically doesn't wait for the addCircle saga to finish executing. Is there any way to wait for it? I call addCircle from inside my components and I didn't have the need to wait it, but in this specific instance I have to call it inside the saga, so I really need to wait for it to finish execution, change the state of the app, so that I can use the updated state in the rest of addTender saga. Any ideas?
As per your code snippet, your addCircle saga will dispatch either addCircleFailedAction or addCircleSucceededAction action creators just before it finishes execution. So we will have to wait for those action in your addTender saga.
Basically, this is what you should do. I'm just guessing your action types based on action creator names.
yield call(addCircleAction(payload.circlePayload));
yield take([ADD_CIRCLE_FAILED_ACTION, ADD_CIRCLE_SUCCEEDED_ACTION]);
// Rest of the saga
There is one edge case though. You are not dispatching any action in the catch block of your addCircle saga. Maybe you can dispatch an action called addCircleExceptionAction inside catch block and wait for it along with the other actions like this:
yield take([ADD_CIRCLE_FAILED_ACTION, ADD_CIRCLE_SUCCEEDED_ACTION, ADD_CIRCLE_EXCEPTION_ACTION]);
If you are dispatching multiple actions that would trigger addRender then there is no guarantee that take(...) would actually wait for the action that resulted of the yield call.
export function* addCircle(circleApi, { payload }) {
try {
const response = yield apply(
circleApi,
circleApi.addCircle,
[payload]
);
if (response.error_type) {
yield put(addCircleFailedAction(response.error));
return response;
} else {
yield put(addCircleSucceededAction(response));
return response;
}
} catch (err) {
console.error(err);
return {err};
}
}
export function* addTender(tenderApi, { payload }) {
try {
//because addCircle saga is returning something you can re use it
// in other sagas.
const result = yield call(addCircle,circleAPI?,payload.circlePayload);
//check for result.error_type here
// Rest of saga removed for brevity.
} catch (err) {
console.error(err);
}
}
Your code and the accepted answer would result in an error because call does not take an action object as first argument (it does take a {context,fn} type object).
Dispatching an action and then listening to another action that may or may not have been a side effect of the action you just dispatched is bad design. You dispatch these actions asynchronously and there is no guarantee they all take the same time to complete or provide the side effect you are waiting for in the same order as they were started.
I am trying to get my app to Server-Side render via the END effect (details on https://github.com/redux-saga/redux-saga/issues/255, with explanations why this is so tricky).
My data relies on 2 async requests: getJwtToken -> (with token data) FetchItem -> now render.
Is this possible at all?
I have spent time looking at Channels (here https://redux-saga.js.org/docs/advanced/Channels.html) and could not get any variation to work.
My Saga looks something like this (LOAD_USER_PAGE is fired initially)
function* loadUserPage() {
yield put({type: 'JWT_REQUEST'})
const { response } = yield call(fetchJwtToken)
if (response) {
yield put({type: 'JWT_REQUEST_SUCCESS', payload: response})
}
}
function* fetchItem() {
console.log('NEVER GETS HERE')
}
function* watchLoadPage() {
yield takeLatest('LOAD_USER_PAGE', loadUserPage);
}
function* watchFetchItem() {
yield takeLatest('JWT_REQUEST_SUCCESS', fetchItem);
}
export default function* rootSaga() {
yield all([
fork(watchLoadPage)
fork(watchFetchItem)
])
}
I believe I understand why it doesn't work (due to END event fired terminating only those effects which have started, and since my 2nd effect will not be fired until my first is back, it is not included in runSaga().done promise.
By doesn't work I mean the action JWT_REQUEST_SUCCESS is fired and the runSaga.done promise runs. But my message in console.log is not fired.
I think its possible by having both requests in the same function, but I am trying to abstract the token auth part out.
Is it not possible in any way?
Really stuck.
EDIT:
The solution was to use channels as suggested in the comment on https://github.com/redux-saga/redux-saga/issues/255#issuecomment-323747994 and https://github.com/redux-saga/redux-saga/issues/255#issuecomment-334231073.
SSR for all :)