6

I find myself puzzled trying to set a very simple rxjs flow of subscriptions. Having multiple non-related subscriptions nested into another.

I'm in an angular application and I need a subject to be filled with next before doing other subscriptions.

Here would be the nested version of what I want to achieve.

subject0.subscribe(a => {

    this.a = a;

    subject1.subscribe(x => {
        // Do some stuff that require this.a to exists
    });

    subject2.subscribe(y => {
        // Do some stuff that require this.a to exists
    });

});

I know that nested subscriptions are not good practice, I tried using flatMap or concatMap but didn't really get how to realize this.

  • 1
    I think the person who voted down this question needs at least to explain why he did so. This is the question that can be raised as you are working with subscriptions and observables. – Meysam Oct 21 '20 at 09:38

3 Answers3

4

You can do that using the concat operator.

const first = of('first').pipe(tap((value) => { /* doSomething */ }));
const second = of('second').pipe(tap((value) => { /* doSomething */ }));
const third = of('third').pipe(tap((value) => { /* doSomething */ }));

concat(first, second, third).subscribe();

This way, everything is chained and executed in the same order as defined.

EDIT

const first = of('first').pipe(tap(value => {
  // doSomething
  combineLatest(second, third).subscribe();
}));
const second = of('second').pipe(tap(value => { /* doSomething */ }));
const third = of('third').pipe(tap(value => { /* doSomething */ }));
first.subscribe();

This way, second and third are running asynchronously as soon as first emits.

Reqven
  • 1,688
  • 1
  • 8
  • 13
  • Thanks for the reply ! I think that will work but I don't want everything to be chained. The second and third subscriptions should only wait for the first one, the third should not wait for the second. – Zacharie Ménétrier Oct 16 '19 at 14:07
4

It's always a good idea to separate the data streams per Observable so you can easily combine them later on.

const first$ = this.http.get('one').pipe(
  shareReplay(1)
)

The shareReplay is used to make the Observable hot so it won't call http.get('one') per each subscription.

const second$ = this.first$.pipe(
  flatMap(firstCallResult => this.http.post('second', firstCallResult))
);

const third$ = this.first$.pipe(
  flatMap(firstCallResult => this.http.post('third', firstCallResult))
);

After this you can perform subscriptions to the Observables you need:

second$.subscribe(()=>{}) // in this case two requests will be sent - the first one (if there were no subscribes before) and the second one

third$.subscribe(() => {}) // only one request is sent - the first$ already has the response cached

If you do not want to store the first$'s value anywhere, simply transform this to:

this.http.get('one').pipe(
  flatMap(firstCallResult => combineLatest([
    this.http.post('two', firstCallResult),
    this.http.post('three', firstCallResult)
  ])
).subscribe(([secondCallResult, thirdCallResult]) => {})

Also you can use BehaviorSubject that stores the value in it:

const behaviorSubject = new BehaviorSubject<string>(null); // using BehaviorSubject does not require you to subscribe to it (because it's a hot Observable)
const first$ = behaviorSubject.pipe(
  filter(Boolean), // to avoid emitting null at the beginning
  flatMap(subjectValue => this.http.get('one?' + subjectValue))
)

const second$ = first$.pipe(
  flatMap(firstRes => this.http.post('two', firstRes))
)

const third$ = first$.pipe(
  flatMap(()=>{...})
)

behaviorSubject.next('1') // second$ and third$ will emit new values
behaviorSubject.next('2') // second$ and third$ will emit the updated values again
Yevhenii Dovhaniuk
  • 1,073
  • 5
  • 11
  • Thanks, I think the first option is what I'm looking for. The use of a `combineLatest` in your second option does not seem to fit my need as two and three should not wait one another. Your third option is very interesting, even if I would opt for a `ReplaySubject(1)` to avoid filtering the initial null value. – Zacharie Ménétrier Oct 16 '19 at 15:39
1

You could do something like this:

subject$: Subject<any> = new Subject();
this.subject$.pipe(
        switchMap(() => subject0),
        tap(a => {
            this.a = a;
        }),
        switchMap(() => subject1),
        tap(x => {
            // Do some stuff that require this.a to exists
        }),
        switchMap(() => subject2),
        tap(y => {
            // Do some stuff that require this.a to exists
        })
    );

if you want to trigger this, simply call this.subject$.next();

EDIT: Here is an possible approach with forkJoin, that shout call the subjects parallel.

subject$: Subject<any> = new Subject();
    this.subject$.pipe(
        switchMap(() => subject0),
        tap(a => {
            this.a = a;
        }),
        switchMap(
            () => forkJoin(
                subject1,
                subject2
        )),
        tap([x,y] => {
          // Do some stuff that require this.a to exists
        })
    );
  • I feel like in this example, subject2 will wait for subject1 before subscribing. In my case I just need subject1 and subject2 to wait for subject0 but subject1 and subject2 should not be chained. Is it the case here ? – Zacharie Ménétrier Oct 16 '19 at 14:11
  • yes you might be right. I think you could put subject1 and subject2 into a forkJoin. I update my answer. But i did not test this yet –  Oct 16 '19 at 14:34