3
votes

I would like to know what is the best way using the RxJS library to execute 3 http requests that depends from the previous result.

Let's imagine that I've 3 services in my Angular application and each of them have a function get(id: number) use to subscribe an observable of the request entity.

I need to call sequencing the first service to get an entity which contains an identifier required for the next call by using the second service which also contains an identifier required for the next call using the third service.


Method 1: Using three subscriptions and set each result to global variables

const firstEntityId = 1;

this.firstService.get(firstEntityId)
  .subscribe((firstEntity: FirstEntity) => {
    this.firstEntity = firstEntity;

    this.secondService.get(firstEntity.secondEntityId)
      .subscribe((secondEntity: SecondEntity) => {
        this.secondEntity = secondEntity;

        this.thirdService.get(secondEntity.thirdEntityId)
          .subscribe((thirdEntity: ThirdEntity) => {
            this.thirdEntity = thirdEntity;

          });
      });
  });

Method 2: Using function with stream and one subscription to set all global variables

const firstEntityId = 1;

this.getFirstSecondThird(firstEntityId)
  .subscribe(([firstEntity, secondEntity, thirdEntity]: [FirstEntity, SecondEntity, ThirdEntity]) => {
    this.firstEntity = firstEntity;
    this.secondEntity = secondEntity;
    this.thirdEntity = thirdEntity;
  });

getFirstSecondThird(id: number): Observable<[FirstEntity, SecondEntity, ThirdEntity]> {
  return this.firstService.get(id).pipe(
    switchMap((firstEntity: FirstEntity) => forkJoin(
      of(firstEntity),
      this.secondService.get(firstEntity.secondEntityId)
    )),
    switchMap(([firstEntity, secondEntity]: [FirstEntity, SecondEntity]) => forkJoin(
      of(firstEntity),
      of(secondEntity),
      this.thirdService.get(secondEntity.thirdEntityId)
    ))
  );
}

In this case, does the method using stream is the fastest one ?

Is there an other way to write my function getFirstSecondThird instead of using switchMap and forkJoin methods ?

(I've seen combineLatest but I didn't found how to pass a parameter from the previous result)

3
combineLatest is more appropriate when the calls are independent, but you need them to occur in sequence. The first method is more readable, what don't you like about it?user8745435
Exactly concerning combineLatest I thought that too and about my two approaches I prefer the second one because it's more reusable if I want to move this function directly into a service to be call in different places. I also prefer when I've only one subscription at the end but I'm still a novice using RxJS and I don't really know about performances.VinceCOT

3 Answers

1
votes

Maybe use map instead subscribe in method 1?

Note, you need to return at all nested levels. In the example I have removed the brackets so the return is implied.

getFirstSecondThird(id: number): Observable<[FirstEntity, SecondEntity, ThirdEntity]> {
  return this.firstService.get(id).pipe(
    mergeMap((first: FirstEntity) => 
      this.secondService.get(first.secondEntityId).pipe(
        mergeMap((second: SecondEntity) => 
          this.thirdService.get(second.thirdEntityId).pipe(
            map((third: ThirdEntity) => [first, second, third])
          )
        )
      )
    )
  )
}

Here is a test snippet,

console.clear()
const { interval, of, fromEvent } = rxjs;
const { expand, take, map, mergeMap, tap, throttleTime } = rxjs.operators;

const firstService = (id) => of(1)
const secondService = (id) => of(2)
const thirdService = (id) => of(3)

const getFirstSecondThird = (id) => {
  return firstService(id).pipe(
    mergeMap(first => 
      secondService(first.secondEntityId).pipe(
        mergeMap(second => 
          thirdService(second.thirdEntityId).pipe(
            map(third => [first, second, third])
          )
        )
      )
    )
  )
}

getFirstSecondThird(0)
  .subscribe(result => console.log('result', result))
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.3.3/rxjs.umd.js"></script>

You might use switchMap() instead of mergeMap() if there is the possibility of getFirstSecondThird() being called a second time but before all the fetches of the first call have completed, and you want to discard the first call - for example in an incremental search scenario.

0
votes

I would make use of the tap operator. It's generally used for debugging purposes, but is great when you need to implement side effects, especially within a chain of observables.

this.firstService.get(firstEntityId).pipe(
  tap((firstEntity: FirstEntity) => this.firstEntity = firstEntity),
  switchMap((firstEntity: FirstEntity) => this.secondService.get(firstEntity.firstEntityId)),
  tap((secondEntity: SecondEntity) => this.secondEntity = secondEntity),
  switchMap((secondEntity: SecondEntity) => this.thirdService.get(secondEntity.secondEntityId))
).subscribe((thirdEntity: ThirdEntity) => {
  this.thirdEntity = thirdEntity;
  // Rest of the code goes here
});

You could even use tap for assigning this.thirdEntity as well, and then use subscribe for subsequent code only.

0
votes

You don't need the forkJoin if you use an inner Observable instead:

getFirstSecondThird(id: string): Observable<[FirstEntity, SecondEntity, ThirdEntity]> {
    return this.firstService.get(id).pipe(
        switchMap(first =>
            this.secondService
                .get(first.secondEntityId)
                .pipe(map(second => [first, second]))
        ),
        switchMap(([first, second]: [FirstEntity, SecondEntity]) =>
            this.thirdService
                .get(second.thirdEntityId)
                .pipe(map(third => <[FirstEntity, SecondEntity, ThirdEntity]>[first, second, third]))
        )
    );
}

Here is the whole code in Context with a test:

type FirstEntity = {id: string, secondEntityId: string};
type SecondEntity = {id: string, thirdEntityId: string};
type ThirdEntity = {id: string};

const FIRST_ENTITY: FirstEntity = {id: 'first', secondEntityId: 'second'};
const SECOND_ENTITY: SecondEntity = {id: 'second', thirdEntityId: 'third'};
const THIRD_ENTITY: ThirdEntity = {id: 'third'};

class X {
    firstService = {get: (id) => of(FIRST_ENTITY)};
    secondService = {get: (id) => of(SECOND_ENTITY)};
    thirdService = {get: (id) => of(THIRD_ENTITY)};

    getFirstSecondThird(id: string): Observable<[FirstEntity, SecondEntity, ThirdEntity]> {
        return this.firstService.get(id).pipe(
            switchMap(first =>
                this.secondService
                    .get(first.secondEntityId)
                    .pipe(map(second => [first, second]))
            ),
            switchMap(([first, second]: [FirstEntity, SecondEntity]) =>
                this.thirdService
                    .get(second.thirdEntityId)
                    .pipe(map(third => <[FirstEntity, SecondEntity, ThirdEntity]>[first, second, third]))
            )
        );
    }
}

describe('X', () => {
    it('getFirstSecondThird', async () => {
        // setup
        const x = new X();
        const firstSpy = spyOn(x.firstService, 'get').and.callThrough();
        const secondSpy = spyOn(x.secondService, 'get').and.callThrough();
        const thirdSpy = spyOn(x.thirdService, 'get').and.callThrough();

        // execution
        const result = await x.getFirstSecondThird('first').pipe(toArray()).toPromise();

        // evaluation
        expect(result[0]).toEqual(<any[]>[FIRST_ENTITY, SECOND_ENTITY, THIRD_ENTITY]);
        expect(firstSpy.calls.allArgs()).toEqual([['first']]);
        expect(secondSpy.calls.allArgs()).toEqual([['second']]);
        expect(thirdSpy.calls.allArgs()).toEqual([['third']]);
    });
});