Angular: How to pipe Observable before subscribing in template

Viewed 2061

Disclaimer: this is closely related to this question.

Currently I have two components, Parent and Child. The Parent passes an Observable to the Child, which subscribes to this Observable in the template to avoid lifecycle/memory leak issues. But I have to pipe the Observable in the Child component to set up a few things.

parent.ts:

public $obs = Observable<any>;
....
loadData() {
   this.obs$ = this.myService.getData();
}

parent.html:

<child [data$]="obs$"></child>

child.ts:

@Input() data$ : Observable<any>;

ngOnChanges(changes) {

  if(changes.data$) {
    console.log("changes detected");
    this.data$.pipe(tap(data => console.log("tap worked")));
  }
}

child.html

<div *ngIf="data$ | async as data;">
{{ data || json }}
</div>

The data is present in the template, the console log shows that the changes have been detected, but the "tap worked" is never shown, leading me to suspect that maybe my call to pipe comes too late. Is there any way to pipe before the template "calls" subscribe?

The idea is having many components similiar to child, all doing different things on the data object. I thought about subscribing to the service in the Parent and passing the json values directly to all childs, but that would require me to write up an *ngIf in all childs.

3 Answers

I suggest, that you don't subscribe to data$ directly. Instead create another Observable you can read.

@Input() data$: Observable<any>;
customData$: Observable<any>;

ngOnChanges(changes) {
  if(changes.data$) {
    console.log("changes detected");
    this.customData$ = this.data$.pipe(tap(data => console.log("tap worked")));
  }
}
<div *ngIf="customData$ | async as data;">
  {{ data || json }}
</div>

Observables by nature are lazy. They are not executed until you subscribe to them. To see the "tap worked" you need to subscribe to it.

this.data$.pipe(tap(data => console.log("tap worked"))).subscribe();

The async by default subscribes and unsubscribes the observable for you, also do note that each subscriber in observable get a new execution context and this behaviour can be overriden.

To cache result in case it's subscribe late you can use either BehaviourSubject or ReplaySubject in your service.

I think the problem is in your ngOnChanges. There you have the condition

  if(changes.data$) {
    console.log("changes detected");
    this.data$.pipe(tap(data => console.log("tap worked")));
  }

this will check if the reference of data$ will change but NOT if data$ emits new values what you actually want. Because of that your tap is not executed.

Instead you could

 public data$: Observable<any>;
  @Input() set data(data$: Observable<any>) {
    this.data$ = data$.pipe(tap(x => console.log("tap worked")));
  }
Related