3

In the code below I have a User object which can have a variable amount of account ids associated with it. When a user gets updated an additional request should be made for each account contained in the user object:

class User {

  id: number;

  accountIds: Array<number>;
}

// Side effects class
@Effect()
update$ = this.actions$
  .ofType('USER_UPDATE')
  .switchMap(action => {

    let user = action.payload;

    for (let accountId of user.accountIds) {
      let account = { id: accountId, userIds: [user.id] };

      // Is there any way to chain these api calls with switch maps
      // and then finally switch map to the user request below?
      return this.apiService.post(`/accounts/${account.id}`, account);
    }

    return this.apiService.post(`/users/${userid}`, user);
  }

Is there any way using RxJS to chain those calls together using switchMap (or something similar) so that the observable isn't considered complete until all subsequent requests have completed without having to write some kind of custom recursive logic?

jcroll
  • 6,875
  • 9
  • 52
  • 66
  • 2
    Maybe you want to use [**`Observable.forkJoin`**](https://stackoverflow.com/questions/38048860/angular2-rxjs-observable-forkjoin) (if order doesn't matter)... store all requests in an `Array` and use like this: `Observable.forkJoin()...` – developer033 May 23 '17 at 21:28
  • Yeah I saw that forkJoin, seems to send all the requests in parallel which is not quite what I'm after but looks like best approach so far. Thanks. – jcroll May 23 '17 at 21:30
  • Those links are good, thanks @martin – jcroll May 24 '17 at 16:22

3 Answers3

5

If you want to handle that only with Observable (which seems to be a good idea within an effect), here's how you could do it :

const { Observable } = Rx;

// DATA
const user = {
  id: 'currentUserId',
  accountIds: [
    'accountId0',
    'accountId1',
    'accountId2',
    'accountId3'
  ]
}

// MOCK BACKEND CALLS
const apiService = {
  post: (url, data) => {
    return Observable.of(`some response for ${url}`).delay(1000);
  }
};

// HANDLE THE MULTIPLE HTTP CALLS
Observable
  .from(user.accountIds)
  .map(accountId => ({ id: accountId, userIds: [user.id] }))
  .map(account => apiService.post(`/accounts/${account.id}`, account))
  .concatAll()
  .do(console.log)
  .last()
  .do(_ => console.log('Finished to chain the HTTP calls for accounts'))
  .switchMap(_ => apiService.post(`/users/${user.id}`, user))
  .do(console.log)
  .subscribe();

The output is :
enter image description here

I've made a Plunkr so you can see it in action :
https://plnkr.co/edit/xFWXyAJM6qm0XwMf4tmv?p=preview


The interesting part here is how we handle the calls.
Let me give you some more details about that :

from allows us to send every accountIds one by one down the observable :

  .from(user.accountIds)

Then we can build for every accountId the Object we want to send to the backend :

  .map(accountId => ({ id: accountId, userIds: [user.id] }))

Once it's done, we create a cold Observable which will make the HTTP call once we subscribe to it :

  .map(account => apiService.post(`/accounts/${account.id}`, account))

Thanks to concatAll, we have the behavior that you're expecting : Making every HTTP calls one by one :

  .concatAll()

Display the response of an HTTP call :

  .do(console.log)

Well now, we'd like to make a final request to users once every request to accounts are done. last will help us wait for the last one (captain obvious here) : .last() Simply log that every requests to accounts are done

  .do(_ => console.log('Finished to chain the HTTP calls for accounts'))

Make the final HTTP call to users .switchMap(_ => apiService.post(/users/${user.id}, user)) Output the response

  .do(console.log)

And of course we need to subscribe as this is a cold Observable otherwise nothing would happen. * .subscribe();

*: In your effect, you might want to simply return the Observable as ngrx/effects will subscribe to it for you ;)

maxime1992
  • 22,502
  • 10
  • 80
  • 121
  • Maxime thank you very much for your detailed answer. I accepted ZahiC's as it is more concise but your answer has merit and I thank you for it and have upvoted it – jcroll May 24 '17 at 16:19
  • I wish more people would give this level of detail in their answers. Not too much or too little. Great answer – tony09uk Mar 09 '19 at 11:15
  • Appreciate the comment! Thanks :) – maxime1992 Mar 10 '19 at 17:14
2

Try this:

update$ = this.actions$
    .ofType('USER_UPDATE')
    .pluck('payload')
    .switchMap(user =>      
        Observable.from(user.accountIds)
            .concatMap(accountId => 
                this.apiService.post(`/accounts/${accountId}`, {id: accountId, userIds: [user.id]})
            )
            .concat(Observable.defer(() => this.apiService.post(`/users/${user.id}`, user)))
    );

Here is an example with hardcoded data: https://jsfiddle.net/96x907ae/1/

ZahiC
  • 13,567
  • 3
  • 25
  • 27
-2

I think you're looking for Observable.concat.

From the docs:

concat joins multiple Observables together, by subscribing to them one at a time and merging their results into the output Observable. You can pass either an array of Observables, or put them directly as arguments. Passing an empty array will result in Observable that completes immediately.

concat will subscribe to first input Observable and emit all its values, without changing or affecting them in any way. When that Observable completes, it will subscribe to then next Observable passed and, again, emit its values. This will be repeated, until the operator runs out of Observables. When last input Observable completes, concat will complete as well. At any given moment only one Observable passed to operator emits values. If you would like to emit values from passed Observables concurrently, check out merge instead, especially with optional concurrent parameter. As a matter of fact, concat is an equivalent of merge operator with concurrent parameter set to 1.

joh04667
  • 7,159
  • 27
  • 34