Kim Gentes
Kim Gentes

Reputation: 1628

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:

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))
);

Upvotes: 2

Views: 5249

Answers (2)

Kim Gentes
Kim Gentes

Reputation: 1628

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]))

Upvotes: 2

Ilsanchez
Ilsanchez

Reputation: 271

With forkJoin you can use the result, don't aware:

forkJoin(
    this.dataService.getShapes(),
    this.dataService.getCircles(),
    this.dataService.getTriangles()
).pipe(
    map(res => {
         // Here you can use the result. That result is an array of last emitted value for each observable. The order is the same that you declare in observable array.
        const [shapes, circles, triangles] = res;
     })
);

One important thing to consider is that if one of request fails, you cannot receive any value and should catch it properly.

Here can see more information forkJoin

Upvotes: 3

Related Questions