Escape from the abyss of callback hell by leveraging the power of Angular, HttpClient, and

I'm currently grappling with understanding Angular (2+), the HttpClient, and Observables. I'm familiar with promises and async/await, and I'm trying to achieve a similar functionality in Angular.

//(...) Here's some example code showcasing how to handle 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 approach I've come up with in Angular looks like this:

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

//(...) Set up component boilerplate.

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

//...

It seems like I'm falling into the Pyramid of Doom pattern with this implementation, which I'd like to avoid. How can I refactor this Angular snippet to prevent that?

Thanks!

P.S: I know about using .toPromise on the result of the .get call, but for now, I want to stick with Observables.

Answer №1

When dealing with observables, the subscribe method is not frequently called. Instead, operators are used to combine observables and create a sequence of operations.

The fundamental operator for transforming the output of one observable into another is map. This is akin to how mapping an array can produce a new array. For example, let's double all values in an observable:

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

Mapping is also essential when transitioning from one http request observable to another. However, there is an additional step required as shown below:

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

The issue with this code is that it generates an observable that emits other observables, creating a 2-dimensional observable-like structure. To address this, we need to flatten it so that the observable outputs results of the second http.get call. Various methods exist for flattening based on the desired order of results if multiple observables emit multiple values:

  • mergeMap allows all observables to run in any order and outputs values as they arrive without canceling old ones.
  • switchMap switches to the latest observable, eliminating race conditions by canceling old ones.
  • concatMap completes the first observable before moving to the next, avoiding race conditions but not canceling old work.

In this scenario, switchMap is recommended. So, the previous code snippet is modified as:

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

Now, let's apply these concepts to your code. The following example illustrates using tools without storing intermediate values like this.result or this.dependentKey:

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}`)
  );
}

// Using it:

getDataFromRemoteServer()
 .subscribe(deeplyNestedResult => {
   // Handle deeplyNestedResult
 });

If saving values is crucial, utilizing the tap operator to indicate side effects is recommended. Tap runs specified code on each value emitted without altering 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
  );
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Guide to implementing Apollo GraphQL subscriptions in NextJS on the client-side

As a newcomer to NextJS, I am facing the challenge of displaying real-time data fetched from a Hasura GraphQL backend on a page. In previous non-NextJS applications, I successfully utilized GraphQL subscriptions with the Apollo client library which levera ...

Encountering a Bad Request Response When Trying to Access WCF Service via Jquery Ajax

Encountered an issue when trying to call a WCF web service using jQuery Ajax, resulting in a bad request error without clear insight into the root cause. The service is not triggering any methods - neither success nor failure. Both the web service and the ...

An effective way to prevent right-clicking on iframes across all websites

I am facing an issue with disabling right click for the iframe. I've successfully disabled it for the default URL of the IFrame, but when displaying any other webpage, the right click remains usable. Below are the sample codes I have used: document.o ...

Limiting Firebase communication to Electron Application

Is there a way to restrict access to a Firebase realtime database or any other database from an Electron application? I understand that you can restrict access by specifying the server your website is hosted on, but since Electron is local, are there any o ...

There appears to be a syntax error in the Values section of the .env file when using nodejs

I have been working on implementing nodemailer for a contact form. After researching several resources, I came across the following code snippet in server.js: // require('dotenv').config(); require('dotenv').config({ path: require(&apos ...

Integrate a post AJAX call into an Angular service for seamless functionality

I have come across an interesting scenario where I have to integrate old ajax code into a new Angular 10 application as per project requirements. Is it possible to directly run the existing ajax calls in the Angular service? Or, is there any node module ...

No control access origin header found on the request to Spring 5 WebFlux functional endpoints

I have scoured numerous resources in search of the perfect solution to my issue. In my opinion, I believe I have all the necessary components in place but I am unable to pinpoint where the problem lies. Utilizing spring 5 with WebFlux and functional endpo ...

Interactive calendar feature with a popup that appears when hovering over an event

I am looking to create a popup on hover with full calendar functionality, similar to the one seen at this link. I have attempted using full calendar with qtip, but I was unable to achieve a clickable popup as it disappears when the mouse moves away. Here ...

"Modify the color of a div element by changing it from the color name to the hexadecimal

Is there a way to use color codes instead of typical color names, like #e06666 for red? content.push('<p><div class="color red"></div>Whole Foods Market</p>'); Any suggestions on how to achieve this? ...

What causes TypeScript to interpret an API call as a module and impact CSS? Encountering a Next.js compilation error

My website development process hit a roadblock when I tried integrating Material Tailwind into my project alongside Next.js, Typescript, and Tailwind CSS. The compilation error that popped up seemed unrelated to the changes, leaving me baffled as to what c ...

Leveraging the power of JavaScript and jQuery to identify comparable SELECT choices

My current approach involves utilizing the .filter() method to match the value of an INPUT field (prjName) with an option in a SELECT field (prjList). However, this method only works when there is an exact match for the option text: $("select[title=' ...

What is the best way to incorporate or reference an existing AngularJS project in a new project?

https://i.stack.imgur.com/2dkC0.png The image suggests that Angular app1 serves as a shared module for both app2 and app3. Is there a way to inject app2 and app3 into the common module? If direct injection is not possible, does anyone have suggestions on ...

What causes the template to refresh when the input remains unchanged while employing the OnPush strategy?

Trying to understand this situation: @Component({ selector: 'app-test', template: `value: {{value|json}} <button (click)="setValue()">set</button>`, changeDetection: ChangeDetectionStrategy.OnPush }) export class TestComponent ...

Ways to extract JSON data from multiple JSON arrays

Within the snippet below, I am attempting to extract the value of women. Right now, I can successfully retrieve 1. Personal care appliances and 2. Jewelry. However, if I try to check any checkbox after that, I encounter an error stating "Uncaught TypeError ...

Troubleshooting: jQuery toggle() issue in Firefox 3.0.12

My jQuery code for toggling is working perfectly in IE6 but not in FF3. I'm wondering what could be causing this issue and if there is a workaround available. <button>Toggle Me</button> <p>Hi</p> <p>Learning jQuery&l ...

Guide to resolving the error "Type 'void' cannot be assigned to type 'Function' in VueJS"

I've created a Vue component that requires a function pointer to execute a delete action. <template> <q-card class="my-card" > <q-img :src="media.normal || media.original"> <div class="absolute ...

Challenge with Sequelize Many-to-Many Query

Currently, I am facing an issue with connecting to an existing MySQL database using Sequelize in Node. The database consists of a products table, a categories table, and a categories_products table. My goal is to fetch products, where each product includes ...

Pictures squeezed between the paragraphs - Text takes center stage while images stand side by side

I'm struggling to figure out how to bring the text between the two images to the front without separating them. The images should be positioned next to each other with a negative square in-between, and the text within this square should be centered b ...

Visuals and PDF Generation Tool

Trying to generate project report pdf's using pdfmake has presented a challenge when it comes to displaying images. A function I have for creating a pdfmake "object" looks like this: function singleProject(data) { return { text: "Project ...

Has the rotation of the model been altered?

Is there a way to detect if the rotation of a model has changed? I've attempted: var oldRotate = this._target.quaternion; console.log('works returns vector3 quaternion: ', oldRotate); var newRotate = oldRotate; if (oldRotate != ...