I have written the following effect in my Angular app which uses rxjs
. On MyActions.myAction
, I receive an object containing a property ids
- an array of ids - and for each id I want to send an HTTP request via this.myApiService.getResource
, which returns an Observable<Resource>
. I want then to collect all results in an array, and dispatch another action passing the array.
public loadResources$: Observable<MyAction> = this.actions$.pipe(
ofType(MyActions.myAction),
switchMap(({ ids }) => from(ids).pipe(
mergeMap(id => this.myApiService.getResource(id)),
toArray()
)),
map(resources) => MyActions.resourcesLoaded({ resources } )),
);
The code above does the job, but I wonder whether I should avoid nesting two flows of reactive operators, and whether there is a better way to write that.
The reason I wonder that is that I am having problems writing a test for it. I wrote the test below but I cannot make it pass.
it('should dispatch an resourcesLoaded action with the resources', () => {
const ids = ['5f7c723832758b859bd8f866'];
const resources = [{} as Resource];
const values = {
l: MyActions.loadResources({ ids }),
t: ids[0],
o: MyActions.resourcesLoaded({ resources })
};
actions = hot('--l------------', values);
const get$ = cold(' -------t-----', values);
const expected = cold('---------o-----', values);
myApiService.getResource.withArgs(ids[0]).returns(get$);
expect(myEffects.loadResources$).toBeObservable(expected);
});
The error I get is:
Expected $.length = 0 to equal 1.
Expected $[0] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: { ....
Error: Expected $.length = 0 to equal 1.
Expected $[0] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: { ....
at <Jasmine>
at compare (http://localhost:9876/Users/jacopolanzoni/Documents/Development/myProject/node_modules/jasmine-marbles/index.js:91:1)
at <Jasmine>
but I wonder whether I should avoid nesting two flows of reactive operators, and whether there is a better way to write that
I'd say it depends on what you want to achieve, at least in this case.
of([1,2,3]).pipe(mergeAll(), switchMap(value => http.get(...)))
differs from
of([1,2,3]).pipe(switchMap(ids => from(ids).pipe(mergeMap(...))))
In the first scenario, each inner observable will be discarded by the next value(except for the last value), so only 3 will resolve.
In the second scenario, it will process all of them, because you explode the array in the inner observable(which is managed by swtichMap
, so the only way its inner observable will be discarded is if a new outer value(e.g another array of ids) is emitted by the source).
A case where nesting is not necessary is:
of([1,2,3])
.pipe(
// whenever you want to explode an array,
// it does not matter which higher order operator you use
// since the operation is **synchronous**
// so, `mergeAll`, `concatAll`, `switchAll` should work the same
mergeAll(),
mergeAll(id => this.apiService.useId(id))
)
// same as
of([1,2,3])
.pipe(
mergeMap(ids => from(ids).pipe(mergeMap(id => this.apiService.useId(id))))
)
As you can see, in this case, switchMap
has been replaced with mergeMap
.