Search code examples
angularrxjsasync-pipe

Timing problem between OnInit and async-pipe


I have a problem with the async-pipe in combination with an Observable which gets it's first value in OnInit. Must be a timing issue about the point in time when OnInit happens and the one when the template gets rendered and thus the Observable get subscribed.

Consider this component:

export class AppComponent implements OnInit {

    subjectA$: Subject<{name:string}>;
    subjectB$: Subject<{name:string}>;

    constructor(
        protected http: HttpClient
    ) {
    }

    ngOnInit() {
        this.subjectA$ = new Subject<{name: string}>();
        this.subjectA$.next({name: "A"});

        this.subjectB$ = new Subject<{name: string}>();
        setTimeout(() => {
          this.subjectB$.next({name: "B"});
        }, 0);
    }

}

and the template:

<p *ngIf='subjectA$ | async as subjectA; else: nosubjectA'>
  subjectA: {{subjectA.name}}
</p>

<ng-template #nosubjectA>
  <p>no subjectA</p>
</ng-template>

<p *ngIf='subjectB$ | async as subjectB; else: nosubjectB'>
  subjectB: {{subjectB.name}}
</p>

<ng-template #nosubjectB>
  <p>no subjectB</p>
</ng-template>

This results in

no subjectA

subjectB: B 

That means: Even if subjectA$ got a value in onInit, the view is not updated. If I wrap around the creation of the first value in a setTimeout as you can see with subjectB$, it works and I see the value. Although this is a solution I am wondering why does this this happen and is there a better solution?

One solution I already found would be using BehaviorSubject instead an provide the first value as initial value:


        this.subjectC$ = new BehaviorSubject<{name: string}>({name: "C"});

leads to subjectC: C with analogous template for subjectC.

Try all on StackBlitz.

My real observable is no Subject at all but the result of a combineLatest-call of different stuff, from which only one is (and have to unfortunately since it is using a value from an @Input()-annotation) a Subject, and manually pushed with next in OnInit as in the example. The rest comes from http et al. Most likely I could wrap the combined result in a BehaviourSubject but it seems ugly and dangerous to me, so it's even worse then the setTimeout approach. But I bet someone can help me out and find a real useful solution. In addition, I would prefer to avoid BehaviorSubject, to prevent developers from being tempted to use getValue.

See on Stackblitz


Solution

  • After the comment I made, I really couldn't help but think that there must be a better way - and finally thought of something that worked!

    I just modified your stackblitz a bit.

    private valueA = "A";
    private valueB = "B";
    
    subjectA$ = of({ name: this.valueA });
    subjectB$ = of({ name: this.valueB });
    subjectC$ = combineLatest([this.subjectA$, this.subjectB$])
              .pipe(
                map((things: [{name:string}, {name:string}]): {name:string} => {return {name: things.map(x => x.name).join('|')}})
              );
    

    This way, we can even discard the ngOnInit hook, and everything works at it should!