Preventing a Pyramid of Doom using rxjs .subscribe

2019-07-23 04:00发布

问题:

I'm currently looking into RxJS's .merge however I'll also ask the question here as I find explanations here at times to be brilliant.

Okay, I have a form that depending on user input opens a modal window, I subscribe to the modal close event and pass back some data that I will use after I call / subscribe to a service method to retrieve some data, then when this has happened I do the same again and call / subscribe another service method to update some date then when this has finished I run a local method. So what I have here is 3 nested .subscribes

const dialogRef = this.matDialog.open(ModalWindowComponent, {});
let userId = 4; // this is the real world is selected by the user
let userData = {}; // this is actually form data created by the user

// dialog is closed
dialogRef.afterClosed().subscribe((result) => {
  if (typeof result === 'string') {
     // subscribe to a service to get some data
     this.userService.getUser(userId).subscribe((user: any) => {
        // do something with the data
        let mergedObj = Object.assign({}, user, {newProperty: result});
          // subscribe to another service to update the data
          this.scbasService.updateUser(userId, mergedObj).subscribe(() => {
             this.doSomethingElse(userData); 
      });
    });
  }
});

What I have here is a "pyramid of doom". I remember when working with AngularJS and working with promises I could return the next service and have chained .then()s. I really want to flatten my code, any ideas?

How could I do the same here so my code doesn't indent constantly?

If I haven't asked or explained myself so well please say so and I will rephrase my question.

回答1:

You could do something like this:

dialogRef
  .afterClosed()
  .filter(result => typeof result === 'string')
  .mergeMap(result => this.userService
    .getUser(userId)
    .mergeMap(user => {
      let mergedObj = Object.assign({}, user, { newProperty: result });
      return this.scbasService.updateUser(userId, mergedObj);
    })
  )
  .do(() => this.doSomethingElse(userData))
  .subscribe();
  • Use filter so that only string results are processed.
  • Use mergeMap to compose an inner observable for the getUser and updateUser calls.
  • Use mergeMap again to merge the inner observable into the outer observable.
  • Use do to do something after the user is updated.
  • And call subscribe. Otherwise, nothing will happen.

Something to keep in mind is that nesting subscribe calls within subscribe calls is an antipattern.

If you want, you could flatten it further, using the result selector in the first mergeMap to add the property:

dialogRef
  .afterClosed()
  .filter(result => typeof result === 'string')
  .mergeMap(
    result => this.userService.getUser(userId),
    (result, user) => Object.assign({}, user, { newProperty: result })
  )
  .mergeMap(
    userWithNewProperty => this.scbasService.updateUser(userId, userWithNewProperty)
  )
  .do(() => this.doSomethingElse(userData))
  .subscribe();