VinceCOT
VinceCOT

Reputation: 33

Angular RxJS best practices to subscribe multiple http requests depending from previous result

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)

Upvotes: 3

Views: 2541

Answers (3)

yankee
yankee

Reputation: 40740

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']]);
    });
});

Upvotes: 0

user8745435
user8745435

Reputation:

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.

Upvotes: 1

Jeto
Jeto

Reputation: 14927

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.

Upvotes: 0

Related Questions