alo-v
alo-v

Reputation: 35

NGRX - Multiple actions on single effect

So this is my effect..

@Effect() uploadImages$ = this.actions$
  .ofType(ProjectActions.UPLOAD_IMAGES)
  .map(action => action.payload)
  .flatMap(project => this.service.uploadImages(project))
    .switchMap((project) => Observable.of(
      this.imageActions.loadImages(),
      this.projectActions.uploadImagesSuccess(project),
    ));

Trying to load a list of images from an API before the uploadImagesSuccess fires.

I've attempted to use .concatMap and .mergeMap instead of .switchMap but that doesn't seem to be what it is.

Pretty new to Effects and Observables, and from what I've come across nothing has lead me to any answers.

Just need to fire one after the other has completed.

Edit 1

So loadImages() fires and once it completes an API call it fires loadImagesSuccess(). I suppose what I'm going for is along the lines of..

@Effect() uploadImages$ = this.actions$
  .ofType(ProjectActions.UPLOAD_IMAGES)
  .map(action => action.payload)
  .flatMap(project => this.service.uploadImages(project))
    .switchMap((project) => Observable.of(
      this.imageActions.loadImages(),
      this.imageActions.loadImagesSuccess(), 
      this.projectActions.uploadImagesSuccess(project),
  ));

And in a more general sense, I'm saving a photo to a project on an API. I want to refresh my ImageState before the uploadImagesSuccess() payload reaches my selector and has an undefined image path due to the ImageState not being updated in time.

Edit 2

I came by this solution to use ForkJoin, https://github.com/ngrx/effects/issues/64

@Effect() uploadImages$ = this.actions$
  .ofType(ProjectActions.UPLOAD_IMAGES)
  .map(action => action.payload)
  .flatMap(project => {
    let projectInfo$ = this.projectService.uploadImages(project);
    let imageInfo$ = this.imageService.loadImages();

    return Observable.forkJoin(projectInfo$, imageInfo$);
  })
  .mergeMap((results: any[]) => {
    let myActions: any[] = [];
    let actionGenerators = [
      (data: any) => {
        return this.projectActions.uploadImagesSuccess(data);
      },
      (data: any) => {
        return this.imageActions.loadImagesSuccess(data);
      }
    ];

    results.forEach((data: any, index: number) => {
      myActions.push(actionGenerators[index](data));
    });

    return Observable.from(myActions);
  });

Not sure if this is the best way to go about this, but it works as long as I delay the API call to load images. Without the delay, the images are being loaded prior to the new images being saved. Currently I'm delaying loadImages() on the API side.

If possible, I would like a better way to handle sending the API call to imageService.loadImages() until projectService.uploadImages() returns. Then once all the data is returned, fire the actions to update the store.

It was suggested to use concat instead of forkJoin in order to wait for one API call to finish before moving on to the next. But this then breaks results.Foreach() because results are no longer in an array.

Upvotes: 2

Views: 6641

Answers (2)

seescode
seescode

Reputation: 2131

Maybe you can try this. Instead of using the load images action just do the load images api call within your upload images side effect. Something like this:

@Effect() uploadImages$ = this.actions$
  .ofType(ProjectActions.UPLOAD_IMAGES)
  .map(action => action.payload)
  .mergeMap(project => this.service.uploadImages(project)
     .mergeMap(() => {
        return this.otherService.loadImages()
          .map((images) => this.projectActions.uploadImagesSuccess({ project: project, images: images))
      }
  ));

Then you can have both reducers update state by listening to the UPLOAD_IMAGES_SUCCESS action:

export default function ProjectReducer (state = initialState, action: Action) {
    switch (action.type) {
        case UPLOAD_IMAGES_SUCCESS:
            return // do your update with action.payload.project
        default:
            return state;
    }
};

export default function ImagesReducer (state = initialState, action: Action) {
    switch (action.type) {
        case UPLOAD_IMAGES_SUCCESS:
            return // do your update with action.payload.images
        default:
            return state;
    }
};

You can see the same pattern in the ngrx examples application. In both reducers ADD_TO_CART is being listened to:

https://github.com/btroncone/ngrx-examples/blob/master/shopping-cart/src/app/reducers/products.ts

https://github.com/btroncone/ngrx-examples/blob/master/shopping-cart/src/app/reducers/cart.ts

Upvotes: 2

ng2user
ng2user

Reputation: 2077

Use concat:

@Effect() uploadImages$ = this.actions$
  .ofType(ProjectActions.UPLOAD_IMAGES)
  .map(action => action.payload)
  .flatMap(project => this.service.uploadImages(project))
    .switchMap((project) => Observable.concat(
      this.imageActions.loadImages(),
      this.projectActions.uploadImagesSuccess(project),
    ));

Or from:

 @Effect() uploadImages$ = this.actions$
      .ofType(ProjectActions.UPLOAD_IMAGES)
      .map(action => action.payload)
      .flatMap(project => this.service.uploadImages(project))
        .switchMap((project) => Observable.from([
          this.imageActions.loadImages(),
          this.projectActions.uploadImagesSuccess(project)]
        ));

Upvotes: 0

Related Questions