Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do combined observables do not update template when using Subject or if they emit after ngAfterContentInit

I define an observable (result$) in a component and I display it on its template via the async pipe. The observable is a combination of other 2 observables (first$, second$) via combineLatest. If one of the observables, or both, emits too early (before ngAfterContentInit I have found), the resulting observable won't emit a value.

Component: does not work

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

result$: Observable<number>;
first$ = new Subject<number>();
second$ = new Subject<number>();

  constructor() {}

  ngOnInit(){
      this.result$ = combineLatest(
        this.first$,
        this.second$
      ).pipe(
        map(([first, second]) => {
          // This is not printed to the console
          console.log('combined obs emitted value');
          return first + second;
        })
      );
   console.log('first and second emit value');
   this.first$.next(2);
   this.second$.next(4);
  }

  ngAfterContentInit() {
      console.log('ngAfterContentInit');
  }
}

The order of execution is:

1.first and second emit value

2.ngAfterContentInit

My assumption here is that in ngAfterViewInit the template has been rendered and the subscriptions have been made. Because the observables emit a value before this, the component is not notified. This can only mean the resulting observable is cold (therefore, you need to subscribe before it emits a value). The 2 observables are Subjects, so my assumption is that subjects are cold observables. Is this correct?

If I delay the emission of first$ and second$, everything works: Component: first$ and second$ emit later @Component({ selector: 'my-app', templateUrl: './app.component.html', styleUrls: [ './app.component.css' ] }) export class AppComponent {

result$: Observable<number>;
first$ = new Subject<number>();
second$ = new Subject<number>();

  constructor() {}

  ngOnInit(){

      this.result$ = combineLatest(
        this.first$,
        this.second$
      ).pipe(
        map(([first, second]) => {
          console.log('combined obs emitted value');
          return first + second;
        })
      );

    // Solution 1: add timeout
    setTimeout(() => {
      console.log('first and second emit value');
      this.first$.next(2);
      this.second$.next(4);
    })

  }

  ngAfterContentInit() {
      console.log('ngAfterContentInit');
  }
}

The order is now:

  1. ngAfterContentInit

  2. first and second emit value

  3. combined obs emitted value

So again, is this because the subscription is made before the observables emit a value?

If I change the observables to BehaviorSubject, everything works too even if the values are emitted before the subscription takes place. Does this mean BehaviourSubjects are hot observables? Component: works

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

result$: Observable<number>;
first$ = new BehaviorSubject<number>(0);
second$ = new BehaviorSubject<number>(0);

  constructor() {

  }

  ngOnInit(){

this.result$ = combineLatest(
        this.first$,
        this.second$
      ).pipe(
        map(([first, second]) => {
          // This is not printed to the console
          console.log('combined obs emitted value');
          return first + second;
        })
      );


  console.log('first and second emit value');
   this.first$.next(2);
   this.second$.next(4);


  }

  ngAfterContentInit() {
      console.log('ngAfterContentInit');
  }
}

Stackblitz

like image 551
MartaGalve Avatar asked Feb 07 '19 00:02

MartaGalve


2 Answers

Q1: The 2 observables are Subjects, so my assumption is that subjects are cold observables. Is this correct?

A1: The answer from this question says that the subject itself is hot. This article describes hot, cold and subjects.

Q2: So again, is this because the subscription is made before the observables emit a value?

A2: Yes. Subscription to Subject will receive values AFTER you subscribe. The delay() you introduced should have given time for this to happen.

Why? This is because a BehaviorSubject always hold a value and emits it when subscribed, while a Subject does not hold a value, it just emits values from producer to current subscribers. Details.

Q3: Does this mean BehaviourSubjects are hot observables?

A: See A1.

Apologies if this does not answer your question directly. I am just trying to say that...when dealing with Subject and BehaviorSubject, I do not really care if they are hot or cold.

Instead, I ask: "Do I want the observable to always hold a value when I subscribe?". If yes, I use a BehaviorSubject. If I am ok with no value upon subscription, then Subject is ok. Other than this difference, both of them will receive emitted values after subscription. --- depends on your use case.

like image 175
kctang Avatar answered Oct 19 '22 20:10

kctang


Use BehaviorSubjects instead of Subjects

https://stackblitz.com/edit/angular-4gnxto?file=src/app/app.component.ts

The async pipe subscribes after the Subjects have emitted. BehaviorSubjects give the lastest result when you subscribe, Subjects only give you a value as they emit.

When the async pipe subscribes to the result$ observable made with combineLatest it will not emit any value from Subjects that have already emitted, it will only combineLatest for Subjects that emit after the subscription is made.

See how in this StackBtitz

https://stackblitz.com/edit/angular-eb7dge?file=src/app/app.component.ts

If you click emit the values are emitted after the async pipe has made the subscription.

like image 34
Adrian Brand Avatar answered Oct 19 '22 19:10

Adrian Brand