2

I think I have read 100+ posts on the topic, and I still cannot figure out how to chain two HttpClient calls using rxjs in Angular 6.

Let's say I have a service with that signature:

  GeoService {
      getState(): Observable<string> {
          return this.http.get<string>(stateURL);
      }
      getCities(state: string): Observable<string[]> {
          return this.http.get<string[]>(citiesURL + state);
      }
   }

I can't for the life of me figure out how to obtain both the state and the corresponding list of cities in my component:

import { Observable } from 'rxjs';
import { map, flatMap, mergeMap, filter, switchMap } from 'rxjs/operators';

...

 ngOnInit() {

      this.svc.getState().
        pipe(map((state) => {
            this.state = state;
            return this.svc.getCities(state);
          }),
          mergeMap((cities) => this.cities = cities))
        ).
        subscribe(console.log('done'));

The code above in one of my 20 random attempts at combining pipe/map/mergeMap/subscribe in every way I could think of... a working example would be really really appreciated :)

Thanks!

Edit: None of the "possible duplicate" posts contain an actual example that works

franck102
  • 221
  • 4
  • 14
  • you could chain them by putting the second (and third and so on) call within the res part (completion if you dont need the res value) of the subscribe to the previous call. eg: `this.svc.getState().subscribe(res=>{this.svc.getCities(res).subscribe(res=>{},err=>{},()=>{})},err=>{},()=>{})` – mast3rd3mon Nov 30 '18 at 09:24
  • Possible duplicate of [Chaining Observables in RxJS](https://stackoverflow.com/questions/37771855/chaining-observables-in-rxjs) – ggradnig Nov 30 '18 at 09:29
  • @ggradnig i wouldnt say so, you can do it from the subscribe part of the http call – mast3rd3mon Nov 30 '18 at 09:31
  • `this.svc.getState().pipe(mergeMap(state => this.svc. getCities(state))).subscribe(...)` – martin Nov 30 '18 at 09:31
  • @martin you need to subscribe to the `getCities()` call else it wont actually make the http call – mast3rd3mon Nov 30 '18 at 09:32
  • @mast3rd3mon you could, but thinking a little bit further, it is well possible that we want to express the chained http calls as **one** Observable (i.e. for use of Angular Async Pipe). – ggradnig Nov 30 '18 at 09:33
  • @ggradnig why complicate it when its a simple answer? – mast3rd3mon Nov 30 '18 at 09:34
  • what's complicated about `mergeMap` ? – ggradnig Nov 30 '18 at 09:36
  • @mast3rd3mon No, that's what `mergeMap` does already. You need just one `subscribe` call at the end of the chain – martin Nov 30 '18 at 09:37
  • his isnt complicated, its just missing out a subscribe call which will mean it will fail, its over complicated to use flatmaps, mergemaps, maps, etc when they arent necessary – mast3rd3mon Nov 30 '18 at 09:37
  • mergemap subscribes internally to the inner observable, there is no "missing" subscribe call. also, you said it isn't complicated and it is complicated in the same sentence. – ggradnig Nov 30 '18 at 09:40
  • if you read it, i said his suggestion isnt complicated, but your recommendation of a dupe and your answer both over complicate what is a simple answer – mast3rd3mon Nov 30 '18 at 09:41
  • martin's suggestion and my answer are de facto identical. his just leaves out the state changes that OP intended to do regardless of how the chaining was implemented. – ggradnig Nov 30 '18 at 09:43
  • mast3rd3mon, ggradnig and martin are not overcomplicating, but fixing your suggestion into something acceptable. You should never ever nest subscriptions. Instead of arguing, you should keep an open mind and you might learn something. – Davy Nov 30 '18 at 18:32
  • Sorry if my last comment was a bit harsh. But you should indeed use switchMap, mergeMap or one of its variants. Nested subscriptions are always avoidable, and you should avoid them. Keep in mind that every subscription needs an unsubscribe. Try to manage that when nesting subscriptions ... – Davy Nov 30 '18 at 18:42

3 Answers3

2

The 21st attempt would have been correct ;-)

this.svc.getState().
    pipe(mergeMap((state) => {
        this.state = state;
        return this.svc.getCities(state);
    }),
    tap((cities) => this.cities = cities)))
.subscribe(() => console.log('done'));

The chained Observable goes inside mergeMap. You can think of it as:

First, map the incoming notifaction to an Observable, then merge the resulting "inner" Observable into the "outer" Observable

Also, use tap instead of map if you intend to change an outside state.

ggradnig
  • 13,119
  • 2
  • 37
  • 61
  • Thanks, I tested our solution and it works. I accepted Kevins' answer since it is simpler, and it doesn't change state from inside mergeMap. – franck102 Nov 30 '18 at 11:12
2

You were almost there:

    this.svc.getState().
        pipe(
            mergeMap((state) => {
                return this.svc.getCities(state).pipe(map(cities => {
                    return { state: state, cities: cities }
                }));
            }),
        ).subscribe(stateAndCities => console.log(stateAndCities));

I advise you to read this article:

https://blog.strongbrew.io/rxjs-best-practices-in-angular/#using-pure-functions

It also explains why you shouldnt interact with global variables in rxjs operators.

Davy
  • 6,295
  • 5
  • 27
  • 38
  • Thanks, tested and it works. I accepted Kevin's answer since it doesn't involve an intermediate object. – franck102 Nov 30 '18 at 11:11
  • I do understand your point about pure functions, but I don't like having to aggregate into a meaningless object just to carry results to the end of the pipe. A pattern that would be natural to me would be: var response = this.svc.getState(); response.subscribe(state => this.state = state); response.pipe(map(state => getProviders(state))).subscribe(cities => ...); Pseudo-code, and I'm so fed up with that API that I don't have the courage to test it... – franck102 Nov 30 '18 at 11:18
  • That would work, but your response object would have to be a ReplaySubject (or an Observable with publishReplay() applied) – Davy Nov 30 '18 at 11:30
1

You can do something like this

this.svc.getState().pipe(
     tap(state=>this.state=state),
     switchMap(this.svc.getCities))
.subscribe(cities=>{
    //got the cities
})

the map operator is here to transform the emited value, but the tap operator is used to do something without modifying emited value of the observable.

note that switchMap(this.svc.getCities) is equivalent to switchMap(state=>this.svc.getCities(state)

Kevin ALBRECHT
  • 494
  • 4
  • 13