I'm still learning observables, so I won't be surprised if there's an easy solution. Basically what I have right now is four nested subscriptions with a fourEach inside the second subscribe(). I saw lots of answers using switchMap, but I couldn't find one that also had a for loop to iterate through. I know I probably should be using nested subscriptions, but I can't figure out how to do it with the forEach.
This is the working code with the nested subscribes:
dialogRef.afterClosed().subscribe(result => {
if(result) {
this.createLikertResponseGroup(result.likertResponseGroup)
.subscribe(likertResponseGroupJSON => {
result.likertResponse.controls.likertResponseFormArray.controls.forEach((element) => {
let characteristic = element.controls.characteristic;
this.newResponseGroupId = likertResponseGroupJSON.LikertResponseGroup.id;
this.createLikertResponse(element, this.newResponseGroupId)
.subscribe(likertResponseJSON => {
if (characteristic) {
let responseId = likertResponseJSON.LikertResponse.id;
this.createCharacteristic(characteristic, this.newResponseGroupId, responseId)
.subscribe(characteristicJSON => {
this.newCharacteristicId = characteristicJSON.Characteristic.id;
});
}
});
});
})
}
});
What I have works right now. So my question is, is it worth changing how I'm doing this? If so, how would I go about it?
I haven't gotten far, but my attempt with switchMap looks like this:
dialogRef.afterClosed().pipe(
filter(result => result != null),
switchMap(result =>
from(result.likertResponse.controls.likertResponseFormArray.controls).pipe(
// not sure what to do after this (or if I'm even doing it right)
)
),
);
mergeMap
Instead of Nested subscribe
mergeMap does everything that a nested subscription does, but it also lets you continue your logic onward as it emits the subscribed values.
Quick aside:
In cases where your subscribed observable emits once and completes (like an http request),
switchMap
andmergeMap
produce the same output.switchMap
is often recommended overmergeMap
in these cases. The reasons range from debugging memory leaks, to marginal performance, to what other developers expect.For simplicity's sake, I've ignored that here and used
mergeMap
in all cases.
You can hide some complexity by nesting mergeMap
and/or nesting subscriptions
because you can rely on functional closures to set and remember values earlier in your pipeline.
It can also become a cause of great confusion down the line. Deeply nested functions are notoriously difficult to debug in JS so the extra effort of mapping into intermediate objects to hold the values you need in the next step (rather than nesting and getting intermediate values via functional closure) is well worth the effort.
It's also marginally faster as the runtime isn't required to travel up the call stack looking for variables (But again, you should do it because it's cleaner, maintainable, and extendable not in order to optimize early).
Here is your code litterally re-written with mergeMap and objects holding intermetiate values:
dialogRef.afterClosed().pipe(
filter(result => result), // <-- only "truthy" results pass same as if(result)
mergeMap(result =>
this.createLikertResponseGroup(result.likertResponseGroup).pipe(
map(likertResponseGroupJSON => ({result, likertResponseGroupJSON}))
)
),
mergeMap(({result, likertResponseGroupJSON}) => merge(
...result.likertResponse.controls.likertResponseFormArray.controls.map(
element => this.createLikertResponse(
element,
likertResponseGroupJSON.LikertResponseGroup.id
).pipe(
map(likertResponseJSON => ({
likertResponseJSON,
characteristic: element.controls.characteristic,
newResponseGroupId: likertResponseGroupJSON.LikertResponseGroup.id
}))
)
)
)),
filter(({characteristic}) => characteristic) // only "Truthy" characteristic allowed
mergeMap(({likertResponseJSON, characteristic, newResponseGroupId}) =>
this.createCharacteristic(
characteristic,
newResponseGroupId,
likertResponseJSON.LikertResponse.id
).pipe(
map(characteristicJSON => ({
newCharacteristicId: characteristicJSON.Characteristic.id,
newResponseGroupId
}))
)
)
).subscribe(({newCharacteristicId, newResponseGroupId}) => {
this.newResponseGroupId = newResponseGroupId;
this.newCharacteristicId = newCharacteristicId;
});
merge
/forkJoin
/concat
Instead of forEach(stream.subscribe())
You'll notice in the code above that when it came time to re-write your forEach loop, I used a combination of merge
and Array#map
instead of Array#forEach
merge
is the closes equivalent to forEach(stream.subscribe())
, but the others can change up behaviour in ways that may even boost performance or just allow you to compose more complex streams intuitively.
Here, lines 2 and 3 have identical output. The second one, however, is easily extended with more RxJS operators
1. const arrayOfStreams = [s1,s2,s3,s4];
2. arrayOfStreams.forEach(s => s.subscribe(console.log));
3. merge(...arrayOfStreams).subscribe(console.log);
extending:
arrayOfStreams.forEach(s => s.subscribe(value => {
if(this.isGoodValue(value)){
console.log(value.append(" end"))
}
}));
merge(...arrayOfStreams).pipe(
filter(this.isGoodValue),
map(value => value.append(" end"))
).subscribe(console.log);