Search code examples
angularrxjsobservablebehaviorsubjectsubject

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


Solution

  • 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.