Search code examples
angularobservablengrx-effectsswitchmap

waiting for multiple observables (in parallel) to complete in an Angular effect


I am writing an effect that requires the results of several separate service calls (returning as observables) before completing it. The first service call must complete before 3 more (which can be async), and then I build my action list. I have it working, but it feels like it is needlessly nested.

@Effect() loadMyData$: Observable<any> = this.actions$.pipe(
    ofType(fromMyData.LOAD_DATA),
    switchMap((action: fromMyData.Load) => {
        return this.dataService.openMyData(action.payload).pipe(
            switchMap(() => this.dataService.getShapes()),
            switchMap(shapeData => {
                return this.dataService.getCircles().pipe(
                    switchMap((circleData) => {
                        return this.dataService.getTriangles().pipe(
                            switchMap((triangleData) => {
                                const flatMyDataWithReferences: any = {
                                    triangleName: triangleData.name,
                                    shapeName: shapeData.name
                                };
                                return [
                                    new fromShapes.Load(),
                                    new fromBalls.Load(),
                                    new fromCircles.LoadListSuccess(
                                        this.organizeCircles(circleData)),
                                    new fromMyData.LoadSuccess(flatMyDataWithReferences),
                                    new fromUi.AddMessage({
                                        type: MessageType.Success, message: 'Successfully loaded my data ' +
                                            shapeData.name +
                                            ' from: ' + action.payload
                                    })
                                ];
                            }),
                            catchError((err) => this.myDataLoadErrorEvents(err))
                        );
                    }),
                    catchError((err) => this.myDataLoadErrorEvents(err))
                );
            }),
            catchError((err) => this.myDataLoadErrorEvents(err))
        );
    }),
    catchError((err) => this.myDataLoadErrorEvents(err))
);

Basically, in my example code here, once I have called and returned from the dataService.openMyData call, I would like to do these calls in parallel:

  • dataService.getShapes
  • dataService.getCircles
  • dataService.getTriangles

Once they all complete, I would like use their returned data within my array of actions to put in the return [new etc...] to wrap up the effect.

hoping someone has a more elegant way to approach the 3 intermediary service calls than this ugly (and needless) indentation hell...

I looked around and found that some people are using forkJoin to wait for the result of multiple observables, but it appears they can't use the result of that forkJoin observable as a return on which to operate. For example, this code below tells me that I am not creating an observable in the end, which is what my effect requires..

@Effect() loadMyData$: Observable<any> = this.actions$.pipe(
    ofType(fromMyData.LOAD_DATA),
    switchMap((action: fromMyData.Load) => {
        return this.dataService.openMyData(action.payload).pipe(
            switchMap(() => {
                return forkJoin(
                        this.dataService.getShapes(),
                        this.dataService.getCircles(),
                        this.dataService.getTriangles()
                    ).pipe(
                        map(joinResult => {
                            const [shapeData, circleData, triangleData] = joinResult;
                            const flatMyDataWithReferences: any = {
                                triangleName: triangleData.name,
                                shapeName: shapeData.name
                            };
                            return [
                                new fromShapes.Load(),
                                new fromBalls.Load(),
                                new fromCircles.LoadListSuccess(
                                    this.organizeCircles(circleData)),
                                new fromMyData.LoadSuccess(flatMyDataWithReferences),
                                new fromUi.AddMessage({
                                    type: MessageType.Success, message: 'Successfully loaded my data ' +
                                        shapeData.name +
                                        ' from: ' + action.payload
                                })
                            ];
                        })
                    );
            }),
            catchError((err) => this.myDataLoadErrorEvents(err))
        );
    }),
    catchError((err) => this.myDataLoadErrorEvents(err))
);

Solution

  • 1. Use forkJoin to Wait for Multiple Observables to Complete

    It turns out my question needed two important solutions to make it work as a proper stream of observable actions, which is what an effect requires as a return. The first part is @llsanchez response- using the return forkJoin().pipe(map(response => response.stuff)); form to complete the effect with the forkJoin, the pipe and the map being the correct sequence.

    2. Depending on Your Needs, use map or mergeMap to Return Action Observable(s) in your Effect

    For things to work properly in an effect with multiple resulting actions (which my example has), you must substitute the map operator with mergeMap as follows:

    @Effect() loadMyData$: Observable<any> = this.actions$.pipe(
        ofType(fromMyData.LOAD_DATA),
        switchMap((action: fromMyData.Load) => {
            return this.dataService.openMyData(action.payload).pipe(
                switchMap(() => {
                    return forkJoin(
                            this.dataService.getShapes(),
                            this.dataService.getCircles(),
                            this.dataService.getTriangles()
                        ).pipe(
                            mergeMap(joinResult => {
                                const [shapeData, circleData, triangleData] = joinResult;
                                const flatMyDataWithReferences: any = {
                                    triangleName: triangleData.name,
                                    shapeName: shapeData.name
                                };
                                return [
                                    new fromShapes.Load(),
                                    new fromBalls.Load(),
                                    new fromCircles.LoadListSuccess(
                                        this.organizeCircles(circleData)),
                                    new fromMyData.LoadSuccess(flatMyDataWithReferences),
                                    new fromUi.AddMessage({
                                        type: MessageType.Success, message: 'Successfully loaded my data ' +
                                            shapeData.name +
                                            ' from: ' + action.payload
                                    })
                                ];
                            })
                        );
                }),
                catchError((err) => this.myDataLoadErrorEvents(err))
            );
        }),
        catchError((err) => this.myDataLoadErrorEvents(err))
    );
    

    In summary, to return a proper stream of Actions in an effect:

    To return a Single Action in an Effect use map, as in the form:

    return forkJoin().pipe(map(response => response.action))
    

    To return Multiple Actions in an Effect use mergeMap, as in the form:

    return forkJoin().pipe(mergeMap(response => [response.action1, response.action2]))