Reputation: 33
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.
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;
});
});
});
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
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
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
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