2

I am currently struggling to wrap my head around angular (2+), the HttpClient and Observables.

I come from a promise async/await background, and what I would like to achieve in angular, is the equivalent of:

//(...) Some boilerplate to showcase how to avoid callback hell with promises and async/await
  async function getDataFromRemoteServer() {
    this.result = await httpGet(`/api/point/id`);
    this.dependentKey = someComplexSyncTransformation(this.result);
    this.dependentResult = await httpGet(`/api/point/id/dependent/keys/${this.dependentKey}`);
    this.deeplyNestedResult = await httpGet(`/api/point/id/dependen/keys/${this.dependentResult.someValue}`);
  }

The best I could come op with in angular is:

import { HttpClient } from `@angular/common/http`;

//(...) boilerplate to set component up.

  constructor(private http: HttpClient) {}

// somewhere in a component.

  getDataFromRemoteServer() {
    this.http.get(`/api/point/id`).subscribe( result => {
       this.result = result;
       this.dependentKey = someComplexSyncTransformation(this.result);
       this.http.get(`/api/point/id/dependent/keys/${this.dependentKey}`).subscribe( dependentResult => {
         this.dependentResult = dependentResult;
         this.http.get(`/api/point/id/dependen/keys/${this.dependentResult.someValue}`).subscribe( deeplyNestedResult => {
            this.deeplyNestedResult = deeplyNestedResult;
         });
       })
    });
  }

//...

As you might have noticed, I am entering the Pyramid of Doom with this approach, which I would like to avoid. So how could I write the angular snippet in a way as to avoid this?

Thx!

Ps: I am aware of the fact that you can call .toPromise on the result of the .get call. But let's just assume I want to go the total Observable way, for now.

3
  • what's with this.result and this.dependentKey? Do you need them anywhere else in the class, outside of this computation? Commented May 1, 2019 at 16:14
  • You could use operators from RxJS to achieve this. I can't determine the type of operation that you are performing in your 'pyramid of doom' so I cannot prescribe any RxJS operator for you, but there are operators that do mapping, filtering etc and you can use them on the initial result Commented May 1, 2019 at 16:14
  • Secondly, you should use a service for this operation. Seems to me that you are accessing the HttpClient from a component Commented May 1, 2019 at 16:15

1 Answer 1

6

When working with observables, you won't call subscribe very often. Instead, you'll use the various operators to combine observables together, forming a pipeline of operations.

To take the output of one observable and turn it into another, the basic operator is map. This is similar to how you can .map an array to produce another array. For a simple example, here's doubling all the values of an observable:

const myObservable = of(1, 2, 3).pipe(
  map(val => val * 2)
);
// myObservable is an observable which will emit 2, 4, 6

Mapping is also what you do to take an observable for one http request, and then make another http request. However, we will need one additional piece, so the following code is not quite right:

const myObservable = http.get('someUrl').pipe(
  map(result => http.get('someOtherUrl?id=' + result.id)
)

The problem with this code is that it creates an observable that spits out other observables. A 2-dimensional observable if you like. We need to flatten this down so that we have an observable that spits out the results of the second http.get. There are a few different ways to do the flattening, depending on what order we want the results to be in if multiple observables are emitting multiple values. This is not much of an issue in your case since each of these http observables will only emit one item. But for reference, here are the options:

  • mergeMap will let all the observables run in whatever order, and outputs in whatever order the values arrive. This has its uses, but can also result in race conditions
  • switchMap will switch to the latest observable, and cancel old ones that may be in progress. This can eliminate race conditions and ensure you have only the latest data.
  • concatMap will finish the entirety of the first observable before moving on to the second. This can also eliminate race conditions, but won't cancel old work.

Like i said, it doesn't matter much in your case, but i'd recommend using switchMap. So my little example above would become:

const myObservable = http.get('someUrl').pipe(
  switchMap(result => http.get('someOtherUrl?id=' + result.id)
)

Now here's how i can use those tools with your code. In this code example, i'm not saving all the this.result, this.dependentKey, etc:

  getDataFromRemoteServer() {
    return this.http.get(`/api/point/id`).pipe(
      map(result => someComplexSyncTransformation(result)),
      switchMap(dependentKey => this.http.get(`/api/point/id/dependent/keys/${dependentKey}`)),
      switchMap(dependantResult => this.http.get(`/api/point/id/dependent/keys/${dependentResult.someValue}`)
    });
  }

// to be used like:

   getDataFromRemoteServer()
     .subscribe(deeplyNestedResult => {
       // do whatever with deeplyNestedResult
     });

If its important to you to save those values, then i'd recommend using the tap operator to highlight the fact that you're generating side effects. tap will run some code whenever the observable emits a value, but will not mess with the value:

  getDataFromRemoteServer() {
    return this.http.get(`/api/point/id`).pipe(
      tap(result => this.result = result),
      map(result => someComplexSyncTransformation(result)),
      tap(dependentKey => this.dependentKey = dependentKey),
      // ... etc
    });
  }
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.