psygo
psygo

Reputation: 7633

How to Nest Multiple Observables/Subscriptions

1. The Problem

I'm trying to create a flow of changing language for my blog and, in sum, this is how it would look like:

  1. The user clicks a button that triggers an event (Subject), i.e., the language change.
  2. This language change is used to query Firebase for the correct page based on that language.

If I do each step separately and, later, the user changes the language, then the query won't be triggered because it doesn't know an event has just happened. So I believe this should be some sort of nested subscription/observable. Am I wrong?

2. My Code So Far

I've tried nesting subscriptions, but it didn't work. I believe it was because of scoping bugs. Right now, this is what I'm trying:

this.headerService.langChanged.pipe(
  map(
    (newLang: string) => {
      this.db
        .collection<AboutMePage>(
          this.aboutMeCollectionName, 
          ref => ref.where('lang', '==', newLang)
        )
        .valueChanges();
  })
)
  .subscribe(
    value => {
      this.aboutMe = value[0]
    }
  );

The headerService.langChanged is a Subject<string>.

Is there an rxjs operator for this use case? concat seemed like a good candidate, but it would have wait for the previous Observable to end, which it doesn't in this case. And there's also the fact that I have to pass parameters onto the next observable...

Upvotes: 1

Views: 1220

Answers (2)

Frederick
Frederick

Reputation: 872

I think there's valid solutions for you using any of concat, switchMap, merge, forkJoin, etc. It really depends on the small things that you want out of them such as concurrency. I put together a little Stackblitz example using forkJoin here: https://stackblitz.com/edit/angular-xyf8a6

You can see in the console logs the different timings of each inner Observable to verify that using forkJoin has it so that it'll only take as long as the slowest observable.

And the subscription as I have it:

this.langChanged
  .subscribe(newLang => {
    console.log(`language changed ${newLang}`);
    this.cancelLangChange.next();
    forkJoin(
      this.getUserName(newLang), 
      this.getUserBio(newLang), 
      this.getUserCreationDate(newLang)
    )
      .pipe(takeUntil(this.cancelLangChange))
      .subscribe(([username, bio, userCreationDate]) => {
        this.aboutMe.username = username;
        this.aboutMe.bio = bio;
        this.aboutMe.userCreationDate = userCreationDate;
      });
  });

I also added a second Subject cancelLangChange which is meant to cancel the forkJoin subscriptions if a user selects another language while it is still loading the first change.

Upvotes: 2

psygo
psygo

Reputation: 7633

I managed to get it to work by using pipe and map basically. The map operation will use the first observable to generate another one based on the former's parameters. Then we subscribe in a nested manner, which will finally give us the data we wanted.

this.aboutMeSubscription = this.headerService.langChanged.pipe(
  map(
    (newLang: string) => {
      return this.db
        .collection<AboutMePage>(
          this.aboutMeCollectionName, 
          ref => ref.where('lang', '==', newLang)
        )
        .valueChanges();
  })
)
  .subscribe(
    langChangedAboutMeObs => {
      this.langChangedSubscription = langChangedAboutMeObs
        .subscribe(
          (value: AboutMePage[]) => {
            this.aboutMe = value[0];
          }
        );
    }
  );

I also changed my Subject to a BehaviorSubject so an initial value has been emitted even before the user does anything. Lastly, it's important to unsubscribe to both subscriptions on ngOnDestroy.

Upvotes: 0

Related Questions