6

I am trying to get my head around Observables in RxJs. I have a page where I need to display all users for a specific site. User and SiteUser entities are in separate API endpoints. I have the following endpoints

userService.getSiteUsers(siteId: string): Observable<SiteUser[]>;

where

export class SiteUser {
    site_id: string;
    user_id: string;
}

and

userService.getUser(user_id: string): Observable<User>;

where

export class User {
    id: string;
    name: string;
    email: string;
    ....
}

So I have to do the following:

  1. Call siteUsers API to get all user ids for specific site
  2. For each user id make a getUser API call to get user details

I can easily do this

let users: User[] = []; // this is bound in html view to a table
this.userService.getSiteUsers("my site id")
                .subscribe((siteUsers) => {
                    for (let siteUser of siteUsers) {
                        this.userService.getUser(siteUser.user_id)
                                        .subscribe((user) => {
                                            users.push(user);
                                        });
                    }
                });

But this approach feels dirty or cumbersome. I am sure there is a much cleaner Observable way of doing it. I am very new to Observables, but as far as I understand I should be able to do something like this (not selectMany and mergeAll function are just my guesses, I tried it and it didn't work, I couldn't even find selectMany in rxjs library))

Get site user observable array -> for each element in observable array create user observable -> merge them all into observable array of users -> subscribe, so something like this:

this.userService.getSiteUsers("my site id") 
                .selectMany((siteUser) => this.userService.getUser(user))
                .mergeAll()
                .subscribe((users) => {
                    this.users = users; 
                });

Can someone please help, I can't get it working

EDIT------

Maybe something like this

this.userService.getSiteUsers("my site id")
    .switchMap(
       (siteUsers) => {
         let userQueries: Observable<User>[] = [];
         for (let siteUser of siteUsers) {
            userQueries.push(this.userService.getUser(siteUser.user_id));
         }

         return Observable.forkJoin(userQueries);
       }
    )
    .subscribe((users) => {
        this.users = users;
    });
fenix2222
  • 4,602
  • 4
  • 33
  • 56
  • @echonax - still feels a bit involved as I first have to get observable of siteusers then do a subscribe and then inside a subscribe build observable array and then do forkjoin. Can I do it all in one chain, i.e. only one subscribe call at the end – fenix2222 Nov 01 '16 at 11:31
  • @echonax - I made a mistake, getSiteUsers returns Observable not Observable – fenix2222 Nov 01 '16 at 11:34

2 Answers2

7

You should use the .flatMap() / .mergeMap() operator if one http call depends on another http call.

For example in your case something like this would do,

this.userService.getSiteUsers("my site id")
.switchMap(
   (siteUsers) => {
     let userQueries: Observable<User>[] = [];
     for (let siteUser of siteUsers) {
        userQueries.push(this.userService.getUser(siteUser.user_id));
     }

     return Observable.forkJoin(userQueries);
   }
)
.subscribe((users) => {
    this.users = users;
});
fenix2222
  • 4,602
  • 4
  • 33
  • 56
eko
  • 39,722
  • 10
  • 72
  • 98
  • In fact, I should be using switchMap, shouldn't I? Then inside switch map I just build an array of observables and use forkjoin to merge them and return forkjoin inside switchmap, then do subscribe which returns users... Is this correct? – fenix2222 Nov 01 '16 at 11:43
  • the `.switchMap()` operator unsubscribes from the previous subscription when the outer observable emits new values, i don't know if thats what you want. forkJoin part is completely correct – eko Nov 01 '16 at 11:48
  • would something like sample in my edit (see bottom of the question) work? – fenix2222 Nov 01 '16 at 11:51
  • Ok, I will try it tomorrow and see if it works. I will mark it as an answer if it works – fenix2222 Nov 01 '16 at 11:56
  • @fenix2222 no problem :) by the way forkJoin will wait for all the observables to finish, fyi. – eko Nov 01 '16 at 11:58
  • Thats actually exactly what I want as I want to further filter and sort array when it is built – fenix2222 Nov 01 '16 at 12:03
1

Try something like this:

this.userService.getSiteUsers("my site id")
  .flatMap((siteUsers) => {
    // map every user into an array of observable requests
    const usersObservables = siteUsers.map(siteUser => this.userService.getUser(siteUser.user_id)).map((res:Response) => res.json())
    return Observable.forkJoin(...usersObservables)
  }).subscribe(users => {
      //you have all your users now;
      console.log(users)
  });

We use the spread operator in here:

return Observable.forkJoin(...usersObservables)

So we transform our array into arguments, like this:

return Observable.forkJoin(observableUser1, observableUser2, observableUser...)
Fabio Antunes
  • 22,251
  • 15
  • 81
  • 96