AWS

Angular 2: HTTP, Observables, and concurrent data loading

How to run multiple concurrent HTTP requests, with the callbacks running only after all of them have completed with Angular 2.


Filed under:

Metal Toad is an AWS Managed Services provider. In addition to Angular work we recommend checking out our article on how to host a website on AWS in 5 minutes.

Angular 2 provides a new pattern for running asynchronous requests, called Observables. Here, we will review a few of the concepts and eventually see how to run multiple concurrent HTTP requests, with the callbacks running only after all of them have completed.

Update, November 27, 2017: This post explains the Http service used in Angular 2. This is now deprecated in favor of the newer HttpClient. This post will remain here as long as Angular 4.x is in long term support. If you are using Angular 5, you should upgrade to the newer HttpClient, as outlined in the post "Angular 5: Making API calls with the HttpClient service".

About Observables and the Http service

Angular 1 developers should be familiar with using Promises to load data asynchronously. Angular 2 uses an a more advanced pattern called Observables. These are objects which can emit one or more data packets. Other objects can subscribe to these Observables and run a callback each time data is emitted. (In this example using the Http service, each Observable will only emit data once, but a different type of Observable could emit data more than once.)

The Observable classes in Angular 2 are provided by the ReactiveX library.

The Http service in Angular 2 is the successor to Angular 1's $http. Instead of returning a Promise, its http.get() method returns an Observable object.

Try this at home!

The source code for this demo application is available on GitHub. It's recommended that you try the Angular 2 Tutorial first, for a basic overview of Angular 2 architecture and Typescript.

For brevity, I have not listed the contents of the JSON data files in this post. They are available in the GitHub repository if needed.

Getting started

To use the HTTP service and Observables, we need to add a few logistics to our index.html and Angular 2 bootstrap file.

index.html:

<html>
  <head>
    <title>Angular2 Http Demo</title>
    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/reflect-metadata/Reflect.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>
    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
  </head>
  <body>
    <demo-app>Loading...</demo-app>
  </body>
</html>

systemjs.config.js:

(function (global) {
  System.config({
    paths: {
      // paths serve as alias
      'npm:': 'node_modules/'
    },
    // map tells the System loader where to look for things
    map: {
      // our app is within the app folder
      app: 'app',
 
      // angular bundles
      '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
      '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
      '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
      '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
      '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
      '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
      '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
      '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
      '@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
 
      // other libraries
      'rxjs':                      'npm:rxjs'
    },
    // packages tells the System loader how to load when no filename and/or no extension
    packages: {
      app: {
        main: './main.js',
        defaultExtension: 'js'
      },
      rxjs: {
        defaultExtension: 'js'
      },
    }
  });
})(this);

Notice the 'rxjs' configuration in systemjs.config.js. This provides the context necessary to use some of our import statements in our Typescript files.

App/main.ts:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
 
platformBrowserDynamic().bootstrapModule(AppModule);

App/app.module.ts:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpModule }    from '@angular/http';
import {FormsModule} from '@angular/forms';
import {DemoService} from './demo.service'
 
import { AppComponent }  from './app.component';
 
@NgModule({
    imports: [BrowserModule,HttpModule,FormsModule],
    declarations: [AppComponent],
    providers: [DemoService],
    schemas: [CUSTOM_ELEMENTS_SCHEMA],
    bootstrap: [AppComponent]
})
export class AppModule { }

Here is where we inject our app-level dependencies.

App/app.component.ts

Our demo app contains only one simple component, which contains a few elements to display some simple data. We will be loading data from a few JSON files, to simulate an API call.

import {Component} from '@angular/core';
import {DemoService} from './demo.service';
import {Observable} from 'rxjs/Rx';
 
@Component({
  selector: 'demo-app',
  template:`
  <h1>Angular2 HTTP Demo App</h1>
  <h2>Foods</h2>
  <ul>
    <li *ngFor="let food of foods">{{food.name}}</li>
  </ul>
  <h2>Books and Movies</h2>
  <h3>Books</h3>
  <ul>
    <li *ngFor="let book of books">{{book.title}}</li>
  </ul>
  <h3>Movies</h3>
  <ul>
    <li *ngFor="let movie of movies">{{movie.title}}</li>
  </ul>
  `
})
export class AppComponent {
 
  public foods;
  public books;
  public movies;
 
  constructor(private http: Http) { }
 
}

Executing a single HTTP request

We can use the HTTP service to request a single resource, by using http.get. This is similar to Angular 1. To do this, we add the following code to our app.component:

  ngOnInit() {
    this.getFoods();
  }
 
  getFoods() {
    this.http.get('/app/food.json')
      .map((res:Response) => res.json())
      .subscribe(
        data => { this.foods = data},
        err => console.error(err),
        () => console.log('done')
      );
  }

We keep the constructor simple here. All it does is initialize the http variable. We'll use the ngOnInit() hook to start the data loading.

Just like Angular 1, we use http.get() to run our HTTP request. This returns an Observable object, which gives us methods like map() for configuring the data processing, and subscribe() for observing the output.

Angular doesn't yet know that we want to parse the response as JSON. We can let it know this by using the .map((res:Response) => res.json())call. This also returns an Observable, useful for method chaining.

To receive the output, we call the subscribe() method. This takes three arguments which are event handlers. They are called onNext, onError, and onCompleted. The onNext method will receive the HTTP response data. Observables support streams of data and can call this event handler multiple times. In the case of the HTTP request, however, the Observable will usually emit the whole data set in one call. The onError event handler is called if the HTTP request returns an error code such as a 404. The onCompleted event handler executes after the Observable has finished returning all its data. This is less useful in the case of the Http.get() call, because all the data we need is passed into the onNext handler.

For more information about the Observable object, see the ReactiveX documentation.

In our example here, we use the onNext handler to populate the component's 'foods' variable.

The error handler just logs the error to the console. The completion callback runs after the success callback is finished.

The handler functions are optional. If you don't need the error or completion handler, you may omit them. If you don't provide an error handler, however, you may end up with an uncaught Error object which will stop execution of your application.

Executing multiple concurrent HTTP requests

Many times, we need to load data from more than one source, and we need to delay the post-loading logic until all the data has loaded. ReactiveX Observables provide a method called forkJoin() to wrap multiple Observables. Its subscribe() method sets the handlers on the entire set of Observables.

To run the concurrent HTTP requests, let's add the following code to our component:

  ngOnInit() {
    ...
    this.getBooksAndMovies(); // <-- add this line
  }
 
  getBooksAndMovies() {
    Observable.forkJoin(
        this.http.get('/app/books.json').map((res:Response) => res.json()),
        this.http.get('/app/movies.json').map((res:Response) => res.json())
    ).subscribe(
      data => {
        this.books = data[0]
        this.movies = data[1]
      },
      err => console.error(err)
    );
  }

Notice that forkJoin() takes multiple arguments of type Observable. These can be Http.get() calls or any other asynchronous operation which implements the Observable pattern. We don't subscribe to each of these Observables individually. Instead, we subscribe to the "container" Observable object created by forkJoin().

When using Http.get() and Observable.forkJoin() together, the onNext handler will execute only once, and only after all HTTP requests complete successfully. It will receive an array containing the combined response data from all requests. In this case, our books data will be stored in data[0] and our movies data will be stored in data[1].

The onError handler here will run if either of the HTTP requests returns an error code.

Refactoring the data loading into a Service

Now that we have seen the entire lifetime of an Observable object, we can refactor part of the logic into a Service for reusability.

I find it useful for the Services to return an Observable, rather than the final data. This allows the component to subscribe to the Observable and either populate the local data variables or show an error message to the user.

First, we create our new Service, called DemoService:

import {Injectable} from '@angular/core';
import {Http, Response} from '@angular/http';
import {Observable} from 'rxjs/Rx';
 
@Injectable()
export class DemoService {
 
  constructor(private http:Http) { }
 
  // Uses http.get() to load a single JSON file
  getFoods() {
    return this.http.get('/app/food.json').map((res:Response) => res.json());
  }
 
  // Uses Observable.forkJoin() to run multiple concurrent http.get() requests.
  // The entire operation will result in an error state if any single request fails.
  getBooksAndMovies() {
    return Observable.forkJoin(
      this.http.get('/app/books.json').map((res:Response) => res.json()),
      this.http.get('/app/movies.json').map((res:Response) => res.json())
    );
  }
 
}

This is the same logic that we formerly had in our component. However, notice that the methods of this service return Observable objects. In our AppComponent, we will subscribe to the onNext, onError, and onComplete callbacks.

After creating our new service, we need to inject it into the application in boot.ts:

...
import {DemoService} from './demo.service' // <-- add this line
 
bootstrap(AppComponent, [
  HTTP_PROVIDERS,
  DemoService  // <-- add this line
]);

Our refactored AppComponent

import {Component} from '@angular/core';
import {DemoService} from './demo.service';
 
@Component({
  selector: 'demo-app',
  template:`
  <h1>Angular2 HTTP Demo App</h1>
  <h2>Foods</h2>
  <div *ngIf="foods_error">An error occurred while loading the foods!</div>
  <ul>
    <li *ngFor="let food of foods">{{food.name}}</li>
  </ul>
  ...
  `
})
export class AppComponent {
 
  ...
  public foods_error:Boolean = false;
 
  constructor(private _demoService: DemoService) { }
 
  ...
 
  getFoods() {
    this._demoService.getFoods().subscribe(
      data => { this.foods = data},
      err => { this.foods_error = true }
    );
  }
 
  getBooksAndMovies() {
    this._demoService.getBooksAndMovies().subscribe(
      data => {
        this.books = data[0]
        this.movies = data[1]
      }
    );
  }
}

Notice that we are now injecting the DemoService into the constructor, instead of the Http object. We also need to import DemoService at the top of the file, instead of Http. We then call methods of this._demoService instead of this.http.

Also notice that we have added an error message in the template. This will be hidden unless the Observable's onError handler runs.

Happy coding!

Learn more about JavaScript in our JavaScript Blog Archive.

Similar posts

Get notified on new marketing insights

Be the first to know about new B2B SaaS Marketing insights to build or refine your marketing function with the tools and knowledge of today’s industry.