tiger
tiger

Reputation: 117

How to write nested subscribe in cleaner way?

I am new with RxJS and I want to learn how to write code using it in clean way. I have nested subscription and I' ve tried to rewrite this, but with no result.

firstMethod() {
  this.testMethod(name)
  console.log(this.currentPerson)
}

testMethod() {
  this.myService.search(name).subscribe(response => {
    if(result) {
      this.currentPerson = result
    } else {
        this.myService.create(name).subscribe(result => {
          const person = {id: result.id, name: name}
          this.currentPerson = person
        })
      }
   })
}

Unfortunatelly despite the code is messy, there is something wrong with some piece after 'else' because console.log shows undefined. Any tips how to fix it?

Upvotes: 3

Views: 3716

Answers (2)

BizzyBob
BizzyBob

Reputation: 14740

To effectively handle nested subscribes, you should use one of the "Higher Order Mapping Operator", which do a few nice things for you:

  1. maps the incoming value to another observable
  2. subscribes to it, so its values are emitted into the stream
  3. manages unsubscribing from these "inner subscriptions" automatically

In this case, switchMap is a good choice because it will only allow a single inner subscription at a time, so whenever myService.search(name) is called a new inner subscription for myService.create(name) is created, and the previous one is automatically unsubscribed.

@Rafi Henig's answer shows a good example of what this could look like.

  • Notice the use of .pipe(). You can define transformations to your observable output using pipeable operators without actually subscribing.

I would suggest you don't subscribe in your testMethod(), but rather return an observable. Also, let's give "testMethod()" a more meaningful name for further discussion: "getPerson()".

getPerson(name: string): Observable<Person> {
  return this.myService.search(name).pipe(
    switchMap(result => {
      return iif(
        () => result,
        of(result),
        this.myService.create(name).pipe(
          map(({ id }) => ({ id, name }))
        )
      )
    }),
    tap(person => this.currentPerson = person)
  );
}

console.log shows undefined. Any tips how to fix it?

1  firstMethod() {
2    this.getPerson(name)
3    console.log(this.currentPerson)
4  }

The reason for the undefined is because the code is asynchronous. Line 2 is executed, then line 3 immediately after, but the async work hasn't been finished yet, so this.currentPerson hasn't been set yet.

since our getPerson() method now returns an observable, we can subscribe and do your console.log() inside the subscribe:

1  firstMethod() {
2    this.getPerson(name).subscribe(
3       () => console.log(this.currentPerson)
4    )
5  }

To simplify, we don't even need this.currentPerson anymore, because the person is emitted through the stream!

1  firstMethod() {
2    this.getPerson(name).subscribe(
3       person => console.log(person)
4    )
5  }

And since you want to...

learn how to write code using it in clean way

I think think cleanest way would probably be to define your "person result" as an observable and ditch this.currentPerson.

person$ = this.getPerson(name);

So now you have this.person$ which can be subscribed to and will always have the current value of person. No need to "manually" update this.currentPerson.

Well... almost. We need to consider what happens when the search term changes.

Let's assume the search term "name" is coming from a form control input.

When using Reactive Forms the input value is an observable source, so we can define our person$ from the search term:

searchTerm$ = this.searchInput.valueChanges();

person$ = this.searchTerm$.pipe(
  switchMap(searchTerm => this.getPerson(searchTerm))
);

getPerson(name: string): Observable<Person> {
  return this.myService.search(name).pipe(
    switchMap(result => {
      return iif(
        () => result,
        of(result),
        this.myService.create(name).pipe(
          map(({ id }) => ({ id, name }))
        )
      )
    })
  );
}

Notice we've defined two different observables, but we haven't subscribed yet! Now, we can leverage the async pipe in our template to handle the subscription, keeping our component code nice and simple.

<p *ngIf="person$ | async as person">
  We found {{ person.name }} !
</p>

I know this has gotten a bit long winded, but I hope you see how its possible to transform output using pipeable operators and how you can define one observable from another.

Upvotes: 4

Rafi Henig
Rafi Henig

Reputation: 6424

Use switchMap to return a new Observable based of the the value of result using IIF operator as demonstrated below:

this.myService.search(name)
  .pipe(
    switchMap(result => {
      return iif(
        () => result,
        of(result),
        this.myService.create(name).pipe(map(({ id }) => ({ id, name })))
      )
    })
  )
  .subscribe(person => {

  })

Or optionally:

this.myService.search(name)
  .pipe(
    switchMap(result => {
      if (result) return of(result);
      else this.myService.create(name).pipe(map(({ id }) => ({ id, name })));
    })
  )
  .subscribe(person => {

  })

Upvotes: 2

Related Questions