robertkroll
robertkroll

Reputation: 8784

Conditionally dispatch an action depending on result of previous action

I have two actions: validate and update. Both can be dispatched independently, but I would also like that if the update action is dispatched, it first dispatches the validate action and only continues if there were no validations.

If this were synchronous code it would be something like:

if (this.backendService.isValid(dataset)) {
    this.backEndService.update(dataset)
}

The definition of the two actions (and their corresponding success and error actions):

export const validateDataset = createAction('[Datasets API] Validate Dataset', props<{ dataset: DataSetDto }>());
export const validateDatasetSuccess = createAction('[Datasets API] Validate Dataset Success', props<{ validationErrors: SgErrorMessage[]}>());
export const validateDatasetFail = createAction('[Datasets API] Validate Dataset Fail', props<{ error: string }>());

export const updateDataset = createAction('[Datasets API] Update Dataset', props<{ dataset: DataSetDto }>());
export const updateDatasetSuccess = createAction('[Datasets API] Update Dataset Success');
export const updateDatasetFail = createAction('[Datasets API] Update Dataset Fail', props<{ errorEntity: SgErrorEntity }>());

Attempt 1

If I have an effect listening for updateDataset and attempts to dispatch first validateDataset it never seems to get used when .pipe() is applied (and probably here I should use a selector like hasValidationErrors returning bool instead of also using filter operator):

updateDataset$ = createEffect(() => {
    return this.actions$.pipe(
        ofType(DatasetApiActions.updateDataset),
        mergeMap(action => of(DatasetPageActions.validateDataset({dataset: action.dataset})).pipe(
            withLatestFrom(this.store.pipe(select(getValidationErrors))),
            filter(([action, validationErrors]) => validationErrors.length > 0),
            mergeMap(([action, validationErrors]) => this.datasetService.editDataSet(action.dataset).pipe(
                map(() => DatasetApiActions.updateDatasetSuccess()),
                catchError(error => of(DatasetApiActions.updateDatasetFail({error})))
            ))
        ))
    );
});

Attempt 2

I tried to instead I split it up into a saveDataset action that should call both validateDataset and updateDataset actions sequentially, and also filter the updateDataset to only run if the validationErrors state is empty. I used concatMap so the actions could trigger sequentially and they probably are but the success/fail responses come in later and so the validation data can't be used:

saveDataset$ = createEffect(() => {
    return this.actions$.pipe(
        ofType(DatasetPageActions.saveDataset),
        concatMap(action => concat(
            of(DatasetPageActions.validateDataset({dataset: action.dataset})),
            of(DatasetApiActions.updateDataset({dataset: action.dataset}))
        ))
    );
});

updateDataset$ = createEffect(() => {
    return this.actions$.pipe(
        ofType(DatasetApiActions.updateDataset),
        withLatestFrom(this.store.pipe(select(getValidationErrors))),
        filter(([action, validationErrors]) => validationErrors.length == 0),
        tap(([action, validationErrors]) => console.log("validationErrors: " + validationErrors))
    );
}, {dispatch: false});

Maybe I'm thinking about it the wrong way and the validation should be built as an async forms validation instead, but I still think the question is valid.

What I'm struggling with is that I can't add an effect to the validateDatasetSuccess action to trigger the updateDataset action because that may be dispatched by the user separately (and not only by saving). Is there any way to achieve this behaviour?

Upvotes: 2

Views: 3100

Answers (1)

Andrei Gătej
Andrei Gătej

Reputation: 11934

You could try this:

updateDataset$ = createEffect(() => {
  return this.actions$.pipe(
    ofType(DatasetApiActions.updateDataset),

    // dispatch the `validate` action
    tap(action => this.store.dispatch(DatasetPageActions.validateDataset({dataset: action.dataset}))),

    switchMap(
      action => this.store.pipe(
        select(getValidationErrors),
        
        // we want to skip the current state
        // after all, the store is just a `BehaviorSubject`,
        // so subscribing to it will return the current state
        skip(1),
        filter(validationErrors => /* ... your condition here ... */),
        mapTo(action),
      ),
    ),
    mergeMap(
      action => this.datasetService.editDataSet(action.dataset).pipe(
        map(() => DatasetApiActions.updateDatasetSuccess()),
        catchError(error => of(DatasetApiActions.updateDatasetFail({error})))
      )
    )
  );
});

Upvotes: 2

Related Questions