Search code examples
angularrxjsangular-materialangular2-observables

How can I use switchMap instead of nested subscriptions?


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)
    )
  ),
);

Solution

  • 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 and mergeMap produce the same output. switchMap is often recommended over mergeMap 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);