8

I'm studying the Observables in Angular2/4, before I've used the Promises for my services call.

I'm wondering what is the best way to make multiple sync calls. Let me explain with an example: my app component have a getUserInfo() method that need to make 3 calls to 3 different services that depends on each other.

getUserId():Number // return the logged in user userId from the session
getUserPermissions(userId):Array<String> // return a list of permission from the userId
getUserInfo(userId):String // return some additional info for the user

Now let suppose that I have an User object as the following:

export class User {
id: number;
name: string;
info: string;
permissions: array<string>;
}

I need to create a new instance of User class with the results of the 3 services call, so I need to run:

  1. getUserId();
  2. getUserPermissions();
  3. getUserInfo();

What is the best and most polite way to accomplish this through Observable?

With promises I would have something like this:

this._service.getUserId().then(r1 => {
  let userId: number = r1;
  this._service.getUserPermissions(userId).then(r2 => {
    let userPermissions: Array<String> = r2;
    this._service.getUserInfo(userId).then(r3 => {
      let userInfo: string = r3;
      let user: User = new User(userId, userInfo, userPermissions);
    });
  })
});
Pennywise83
  • 1,784
  • 5
  • 31
  • 44

1 Answers1

14

I can't guarantee that this is the best or most polite way because RxJs is such a powerful library where you can achieve the same result in many different ways, but I'll give it a shot.
I will chip in two options.

Assuming your service looks something like this:

userservice.ts

@Injectable()
export class UserService {

  constructor(private http: Http) { }

  getUserId(): Observable<{ id: number, name: string }> {
    return Observable.of({ id: 3, name: 'Bob' }).delay(300);
    /* 
     * replace with http:
     * return this.http.get('http://my.api.com/api/user/id').map(res => res.json());
     */
  }

  getUserPermission(userId: number): Observable<{ permissions: string[] }> {
    return Observable.of({ permissions: ['user', 'admin'] }).delay(300);
    /* return this.http.get(`http://my.api.com/api/user/${userId}/permissions`).map(res => res.json()); */
  }

  getUserInfo(userId: number): Observable<{ info: string }> {
    return Observable.of({ info: 'is a nice person'}).delay(300);
    /* return this.http.get(`http://my.api.com/api/user/${userId}/info`).map(res => res.json()); */
  }
}

Notice that the methods return Observables of JSON-objects!
Since Angular http already returns Observables, it is probably the easiest and cleanest to keep it an Observable-chain all the way down.
Of course you could use the map-operator (f.e. .map(result => result.info)) inside of the service method to make the return type to Observable<string> instead of Observable<{ info: string }>.


switchMap

This approach is suited for requests that have to happen in a specific order.

this.userService.getUserId()
  .switchMap(userResult =>
    this.userService.getUserPermission(userResult.id)
    .switchMap(permissionsResult =>
      this.userService.getUserInfo(userResult.id)
        .map(infoResult => ({
          id: userResult.id,
          name: userResult.name,
          permissions: permissionsResult.permissions,
          info: infoResult.info
        }))
    )
  )
  .subscribe(v => console.log('switchmap:', v));

If you open the network-tab of your browser you will see that the requests are executed in sequence, meaning that each request has to finish before the next one starts. So getUserId() has to finish before getUserPermission() starts, which in turn has to finish before getUserInfo() can run... and so on.

You can also use mergeMap instead. The only difference is, that switchMap can cancel an ongoing http-request when a new value is emitted by the source observable. Look here for a good comparison.

forkJoin

This approach allows you to execute requests in parallel.

this.userService.getUserId()
  .switchMap(userResult => Observable.forkJoin(
    [
      Observable.of(userResult),
      this.userService.getUserPermission(userResult.id),
      this.userService.getUserInfo(userResult.id)
    ]
  ))
  .map(results => ({
    id: results[0].id,
    name: results[0].name,
    permissions: results[1].permissions,
    info: results[2].info
  }))
  .subscribe(v => console.log('forkJoin:', v));

Since forkJoin runs all the Observable sequences it has been given in parallel, it is the better option if the requests (or at least some of them) don't depend on each other.
In this example, the getUserId()-request will run first, and once it has finished both getUserPermission() and getUserInfo() will start running in parallel.


Both methods will return an object with the following structure:

{
    "id": 3,
    "name": "Bob"
    "permissions": [
        "user",
        "admin"
    ],
    "info": "is a nice person"
}
mtx
  • 1,662
  • 1
  • 18
  • 23
  • This is a great answer – Jesse Carter Jun 07 '17 at 20:41
  • Brillant answer. Thank you a lot! – Pennywise83 Jun 08 '17 at 08:35
  • If they both run in parallel, does the order vary? i.e can "info" come before "permission"? or the other way around? Is there a way to assign key to each request whilst maintaining parallelism? – Julian Nov 13 '17 at 18:39
  • @Julian The order of the Observables you pass into the `forkJoin`-input-array is up to you, as long as you access the result-array in that order. So if you want to pass in `getUserInfo()` before `getUserPermission()` you will have to access the permissions with index `[2]` and info with `[1]`. As far as I'm aware (and checking the source-code) you cannot assign 'custom' keys to the result array, because internally it is a simple [for-loop with a counter](https://github.com/Reactive-Extensions/RxJS/blob/master/src/core/linq/observable/forkjoin.js#L30) where each results gets assigned it's index – mtx Nov 13 '17 at 19:01
  • @mtx So if i know the order the request is made the result will be in the same order? – Julian Nov 14 '17 at 09:50
  • 1
    @Julian Yes, you can depend on the result array having the same order as the input array. Just to be clear, because I'm not sure if you are asking something different than I initially thought, the result is available as soon as all requests are finished, not before. So while the requests in forkJoin start and run in parallel, they can finish at different times. You will get the result as soon as all requests are finished. If that was your question, maybe the [rxjs-marble diagram](http://reactivex.io/documentation/operators/images/forkJoin.png) is helpful. – mtx Nov 14 '17 at 18:57