Search code examples
typescriptreduxgeneratorredux-sagasaga

Redux Saga nested Generator function effects don't runs in sequence using yield put (typescript)


I want to perform two async actions and, when both have finished processing, a third action. To do this I created three saga workers: the first updates the email field on DB:

export function* emailUpdateRequestSaga(action: IEmailUpdateRequest) {
  const requestURL = '/updateEmail';
  const requestData = {
    userId: action.userId,
    email: action.email
  };
  try {
    const {data, status}: Pick<AxiosResponse, 'data' | 'status'> = yield call(
      update,
      requestURL,
      requestData
    );

    yield put(emailUpdateSuccess({data, status}));
  } catch (err) {
    console.log('err', err);
    yield put(emailUpdateFail(err));
  }
}

the second sends email:

export function* genericEmailRequestSaga(action: IGenericEmailRequest) {
  const requestURL = '/sendEmail';

  const requestOpt = {
    headers: {},
    body: {
      email: action.email
    }
  };

  try {
    const {data, status}: Pick<AxiosResponse, 'data' | 'status'> = yield call(
      post,
      requestURL,
      requestOpt
    );

    yield put(genericEmailSuccess({data, status}));
  } catch (err) {
    console.log('err', err);
    yield put(genericEmailFail(err));
  }
}

then, the third wraps all and put success action:

export function* emailSendAndUpdateRequestSaga(action: IEmailSendAndUpdateRequest) {
  try {
    // first generator
    yield put(emailUpdateRequest(action.userId, action.email));

    // second generator
    yield put(genericEmailRequest(action.email));

    // success action
    yield put(emailSendAndUpdateSuccess(true));

  } catch (err) {
    console.log('err', err);
    yield put(emailSendAndUpdateFail(err));
  }
}

this is the watcher:

export function* sagas() {
  yield takeEvery(EmailActionEnum.SEND_EMAIL_REQUEST, genericEmailRequestSaga);
  yield takeEvery(EmailActionEnum.EMAIL_UPDATE_REQUEST, emailUpdateRequestSaga);
  yield takeEvery(EmailActionEnum.EMAIL_SEND_AND_UPDATE_REQUEST, emailSendAndUpdateRequestSaga);
}

the problem is that in the emailSendAndUpdateRequestSaga the yield put(emailSendAndUpdateSuccess(true)); is fired after the previous requests starts but even if they fail I still have success action fired.

I want to fire the third action only if the previous generators have finished processing without errors, how can I do this?


Solution

  • // first generator
    yield put(emailUpdateRequest(action.userId, action.email));
    
    // second generator
    yield put(genericEmailRequest(action.email));
    

    While these lines do indirectly cause the other sagas to run, the only thing they directly do is dispatch an action. Dispatching an action is synchronous, and so this code will not wait at all before moving on.

    If you want to leave the two sagas exactly the way they currently are, then you could use take to listen for the actions that those sagas will eventually dispatch in order to pause your main saga. For example:

    export function* emailSendAndUpdateRequestSaga(action: IEmailSendAndUpdateRequest) {
      try {
        yield put(emailUpdateRequest(action.userId, action.email));
        const action = yield take([
          // I'm guessing the action types are constants something like this, but they weren't in your sample code.
          EmailActionEnum.EMAIL_UPDATE_SUCCESS, 
          EmailActionEnum.EMAIL_UPDATE_FAIL
        ]);
        if (action.type === EmailActionEnum.EMAIL_UPDATE_FAIL) {
          throw action;
        }
    
        yield put(genericEmailRequest(action.email));
        const action = yield take([
          EmailActionEnum.SEND_EMAIL_SUCCESS, 
          EmailActionEnum.SEND_EMAIL_FAIL
        ]);
    
        if (action.type === EmailActionEnum.SEND_EMAIL_FAIL) {
          throw action;
        }
    
        // success action
        yield put(emailSendAndUpdateSuccess(true));
    
      } catch (err) {
        console.log('err', err);
        yield put(emailSendAndUpdateFail(err));
      }
    }
    

    I think a better solution though would be to change your approach so instead of dispatching actions, you call the sagas directly. Combined with modifying the sagas so that they throw when there's an error, you can do the following:

    export function* emailUpdateRequestSaga(action: IEmailUpdateRequest) {
      const requestURL = '/updateEmail';
      const requestData = {
        userId: action.userId,
        email: action.email
      };
      try {
        const {data, status}: Pick<AxiosResponse, 'data' | 'status'> = yield call(
          update,
          requestURL,
          requestData
        );
    
        yield put(emailUpdateSuccess({data, status}));
      } catch (err) {
        yield put(emailUpdateFail(err));
        throw err; // <---- added this to rethrow the error
      }
    }
    
    export function* genericEmailRequestSaga(action: IGenericEmailRequest) {
     // ... code omitted. Add a throw like in emailUpdateRequestSaga
    }
    
    export function* emailSendAndUpdateRequestSaga(action: IEmailSendAndUpdateRequest) {
      try {
        // Changed to `call` instead of `put`
        yield call(emailUpdateRequestSaga, emailUpdateRequest(action.userId, action.email));
    
        // Changed to `call` instead of `put`
        yield call(genericEmailRequestSaga, genericEmailRequest(action.email));
    
        // success action
        yield put(emailSendAndUpdateSuccess(true));
    
      } catch (err) {
        console.log('err', err);
        yield put(emailSendAndUpdateFail(err));
      }
    }
    

    yield call(/*etc*/) will run the specified saga until it finishes. The main saga will not move on until it returns or throws.