Angular 2: HTTP, Observables, and concurrent data loading

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.

Comments

thank you very much for taking out time to article this awesome article, I'm new to observable, this is good place to learn about observable.forkJoin. Appreciate it greatly!!

Hi,
I have written forkJoin as shown below.It is not working (and no errors too) if I'll not set api method as first.Can you tell me why?

my.ts

    let getMyBookMarksApi = this.userService.getMyBookMarks(this.page);
    let getMyTopicsApi = this.userService.myTopic();
 
    Observable.forkJoin([getMyBookMarksApi, getMyTopicsApi])
        .subscribe(res => {
            this.setMyProfile(res[0]);
            this.arrangeMyTopics(res[1]);
        },
        error => { },
        () => { });

service.ts

 myTopic() {
 return this.api.get(config.myTopics).map((res: any) => res.json()).first();//without first forkjoin is not working.Why?
 }

Api response is just like this:

{
  "num_of_definitions": 11,
  "categories": [
    {
      "id": 68,
      "name": "Founders",
      "num_of_definitions": 1,
      "icon": {
        "large": ""
      },
      "from_color": "#1E3C72",
      "to_color": "#2A5298"
    },
    {
      "id": 27,
      "name": "Innovation",
      "num_of_definitions": 1,
      "icon": {
        "large": ""
      },
      "from_color": "#EE0979",
      "to_color": "#FF6A00"
    },
    {
      "id": 58,
      "name": "Life success",
      "num_of_definitions": 1,
      "icon": {
        "large": ""
      },
      "from_color": "#D53369",
      "to_color": "#CBAD6D"
    },
 
  ]
}

Hi,
I have a scenario where i need to enroll multiple user in system. for that i need to first call getUser route and based on its response need to identify few properties exist or not if not then again call multiple routes(this property adding can be async), and if user not exit create a user.
In this scenario my entire logic depend of getUser result. this all needs to be perform in sync manner. How to use forkjoin.

Please help me on below issue:

Scencario is, Parent component uses service class to call REST service and emmit reponse to the child components then child component subscribes it and displays data on UI, now problem here is getting reposne from REST service gets time and in between UI components get loadded which causes object properties undefined errors.

How to handle this situation

Sample JSON response onject is as belwo

{
"name" : "XYZ",
"age" : "25",
"address":{
"street": "ABC Road",
"city":"New York",
"state":"NY",
"PCd":"132132"
}
}

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <cpp>, <java>, <php>. The supported tag styles are: <foo>, [foo].
  • Web page addresses and email addresses turn into links automatically.
  • Lines and paragraphs break automatically.

Ready for transformation?