103

I'm currently trying to teach myself Angular2 and TypeScript after happily working with AngularJS 1.* for the last 4 years! I have to admit I am hating it but I am sure my eureka moment is just around the corner... anyway, I have written a service in my dummy app that will fetch http data from a phoney backend I wrote that serves JSON.

import {Injectable} from 'angular2/core';
import {Http, Headers, Response} from 'angular2/http';
import {Observable} from 'rxjs';

@Injectable()
export class UserData {

    constructor(public http: Http) {
    }

    getUserStatus(): any {
        var headers = new Headers();
        headers.append('Content-Type', 'application/json');
        return this.http.get('/restservice/userstatus', {headers: headers})
            .map((data: any) => data.json())
            .catch(this.handleError);
    }

    getUserInfo(): any {
        var headers = new Headers();
        headers.append('Content-Type', 'application/json');
        return this.http.get('/restservice/profile/info', {headers: headers})
            .map((data: any) => data.json())
            .catch(this.handleError);
    }

    getUserPhotos(myId): any {
        var headers = new Headers();
        headers.append('Content-Type', 'application/json');
        return this.http.get(`restservice/profile/pictures/overview/${ myId }`, {headers: headers})
            .map((data: any) => data.json())
            .catch(this.handleError);
    }

    private handleError(error: Response) {
        // just logging to the console for now...
        console.error(error);
        return Observable.throw(error.json().error || 'Server error');
    }   
}

Now in a Component I wish to run (or chain) both getUserInfo() and getUserPhotos(myId) methods. In AngularJS this was easy as in my controller I would do something like this to avoid the "Pyramid of doom"...

// Good old AngularJS 1.*
UserData.getUserInfo().then(function(resp) {
    return UserData.getUserPhotos(resp.UserId);
}).then(function (resp) {
    // do more stuff...
}); 

Now I have tried doing something similar in my component (replacing .then for .subscribe) however my error console going crazy!

@Component({
    selector: 'profile',
    template: require('app/components/profile/profile.html'),
    providers: [],
    directives: [],
    pipes: []
})
export class Profile implements OnInit {

    userPhotos: any;
    userInfo: any;

    // UserData is my service
    constructor(private userData: UserData) {
    }

    ngOnInit() {

        // I need to pass my own ID here...
        this.userData.getUserPhotos('123456') // ToDo: Get this from parent or UserData Service
            .subscribe(
            (data) => {
                this.userPhotos = data;
            }
        ).getUserInfo().subscribe(
            (data) => {
                this.userInfo = data;
            });
    }

}

I'm obviously doing something wrong... how would I best with Observables and RxJS? Sorry if I am asking stupid questions... but thanks for the help in advance! I have also noticed the repeated code in my functions when declaring my http headers...

Mike Sav
  • 14,805
  • 31
  • 98
  • 143

2 Answers2

153

For your use case, I think that the flatMap operator is what you need:

this.userData.getUserPhotos('123456').flatMap(data => {
  this.userPhotos = data;
  return this.userData.getUserInfo();
}).subscribe(data => {
  this.userInfo = data;
});

This way, you will execute the second request once the first one is received. The flatMap operator is particularly useful when you want to use the result of the previous request (previous event) to execute another one. Don't forget to import the operator to be able to use it:

import 'rxjs/add/operator/flatMap';

This answer could give you more details:

If you want to only use subscribe method, you use something like that:

this.userData.getUserPhotos('123456')
    .subscribe(
      (data) => {
        this.userPhotos = data;

        this.userData.getUserInfo().subscribe(
          (data) => {
            this.userInfo = data;
          });
      });

To finish, if you would want to execute both requests in parallel and be notified when all results are then, you should consider to use Observable.forkJoin (you need to add import 'rxjs/add/observable/forkJoin'):

Observable.forkJoin([
  this.userData.getUserPhotos(),
  this.userData.getUserInfo()]).subscribe(t=> {
    var firstResult = t[0];
    var secondResult = t[1];
});
W. van Kuipers
  • 1,400
  • 1
  • 12
  • 26
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • However I do get the error 'TypeError: source.subscribe is not a function in [null]' – Mike Sav Feb 08 '16 at 12:49
  • Where do you have this error? When calling `this.userData.getUserInfo()`? – Thierry Templier Feb 08 '16 at 12:51
  • From the `Observable.forkJoin` – Mike Sav Feb 08 '16 at 12:54
  • 3
    Oh! There was a typo in my answer: `this.userData.getUserPhotos(), this.userData.getUserInfo()` instead of `this.userData.getUserPhotos, this.userData.getUserInfo()`. Sorry! – Thierry Templier Feb 08 '16 at 12:56
  • @ThierryTemplier I'm using flatMap and subscribe, I'm getting data in my flatMap but not in the subscribe. The data => in subscribe is undefined. I tested the function in subscribe by itself and it works. I also know that the subscribe part is executing by way of logging a console message. Thoughs? thx – Adam Mendoza Dec 20 '16 at 05:08
  • @AdamMendoza do you return something into the `flatMap` callback? This returned value must be an observable... Hope it helps you ;-) – Thierry Templier Dec 20 '16 at 10:23
  • @ThierryTemplier My code looks just like your example at the top of your response. I think the problem is that the component is being built before the data is ready. I loading the data by calling a service via a function. Is this not the correct way to load data? ngOnInit() { this.getContext(); } getContext(){ // call my service} – Adam Mendoza Dec 21 '16 at 03:29
  • 1
    why flatMap? Why not switchMap? I would assume that if the initial GET request suddenly outputs another value for User, you wouldn't want to keep getting images for the previous value of User – f.khantsis May 09 '17 at 22:21
  • What is the usage of this.userPhotos = data if I cannot access this.userPhotos in other functions to filter and store it to other variables. – Janatbek Orozaly Aug 01 '17 at 05:27
  • For forkJoin to work you have to add `import 'rxjs/add/observable/forkJoin'` – Tadija Bagarić Oct 25 '17 at 10:22
  • 4
    For anyone looking at this and wondering why it isn't working: in RxJS 6 the import and forkJoin syntax have slightly changed. You now need to add `import { forkJoin } from 'rxjs';` to import the function. Also, forkJoin is no longer a member of Observable, but a separate function. So instead of `Observable.forkJoin()` it's just `forkJoin()`. – Bas Nov 07 '18 at 19:44
  • 1
    It just bothers me a lot, that it doesn't seem very clean, and I was trying to look for a different solution. To be more clear: you have the function getUserPhotos(). Inside it you do getUserInfo(). Then it's either rename the first function and say getUserPhotosAndAlsoGetUserInfo(), or maybe there is another way. Because for me it doesn't look like getUserInfo belongs inside getUserPhotos. In AngularJs, with promises, it was clear, getUserInfo.then(getUserPhotos). Here it feels like they're all in a big bowl... – bokkie Jan 13 '21 at 11:40
1

What you actually need is the switchMap operator. It takes the initial stream of data (User info) and when completed, replaces it with the images observable.

Here is how I'm understanding your flow:

  • Get User info
  • Get User Id from that info
  • Use the User Id to Get User Photos

Here is a demo. NOTE: I mocked the service but the code will work with the real service.

  ngOnInit() {
    this.getPhotos().subscribe();
  }

  getUserInfo() {
    return this.userData.getUserInfo().pipe(
      tap(data => {
        this.userInfo = data;
      }))
  }
  getPhotos() {
    return this.getUserInfo().pipe(
      switchMap(data => {
        return this.userData.getUserPhotos(data.UserId).pipe(
          tap(data => {
            this.userPhotos = data;
          })
        );
      })
    );
  }

austinthedeveloper
  • 2,401
  • 19
  • 27