3

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.

Hafnernuss
  • 2,659
  • 2
  • 29
  • 41
  • 1
    This sounds confusing, but you could write a `pipe` Angular pipe which takes a rxjs `pipe()` (the static one from the rxjs entry point) as its argument and similarly to the async pipe takes care of dealing with when the input observable changes. Then use it like `data$ | pipe: yourPipes | async` with `yourPipes = pipe(tap(...))` – Ingo Bürk Nov 28 '20 at 12:47
  • As for your current attempt, operators always return a new observable, they don't mutate the original one. Also I don't actually recommend my approach above, but it should work. I'd probably prefer not passing observables as inputs since that generally requires assumptions in the child component, eg about it being replayed. – Ingo Bürk Nov 28 '20 at 12:49
  • Could it be that you are declaring a variable named `$obs`, and assigning the output of `this.myService.getData()` to `this.obs$`? Or it that just a typo in the post? – R. Richards Nov 28 '20 at 13:11
  • @R.Richards no thats not a typo, thats what I'm doing. Is that wrong? – Hafnernuss Nov 28 '20 at 13:52
  • @IngoBürk thanks for your suggestion. This sounds... a bit overly complicated. For now I am passing the `plain` data to the children. – Hafnernuss Nov 28 '20 at 13:53
  • @R.Richards of course, the service returns an Observable. – Hafnernuss Nov 28 '20 at 14:02

3 Answers3

1

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>
MoxxiManagarm
  • 8,735
  • 3
  • 14
  • 43
  • Suppose I have multiple child components... this would still cause one network request for each component, since they all subscribe individually, right? – Hafnernuss Nov 28 '20 at 14:59
  • Yes likely, but you could pipe the original observable with `shareReplay(1)`. With `shareReplay(1)` they wouldn't. – MoxxiManagarm Nov 28 '20 at 15:49
  • do you have any example for that? Are there any obvious advantages/disadvantages of using this instead of passing the data "directly"? Or does it boil down to personal preference? – Hafnernuss Nov 28 '20 at 15:50
  • 1
    I'd say rather personal preference. But a good practice are dumb components and it sounds like your child components are dumb. https://medium.com/@jtomaszewski/how-to-write-good-composable-and-pure-components-in-angular-2-1756945c0f5b – MoxxiManagarm Nov 28 '20 at 20:07
0

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.

alt255
  • 3,396
  • 2
  • 14
  • 20
  • This (somewhat) explains why tap isn't executed, but it doesn't actually answer the question. – Ingo Bürk Nov 28 '20 at 12:49
  • Well, but I am subscribing, via the async in the templae. I was also thinking about passing a BehaviourSubject.asObservable() to the children and subscribe to those instead, but that still wouldn't solve the problem of the pipe coming "too late". – Hafnernuss Nov 28 '20 at 13:55
0

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")));
  }
Mikelgo
  • 483
  • 4
  • 15
  • this... would work? I've never seen something like that. Do you have any reference for that? – Hafnernuss Dec 05 '20 at 09:41
  • Sure. here you go: https://stackblitz.com/edit/angular-ivy-s1exdi?file=src%2Fapp%2Fapp.component.html . In the hurry I did a mistake in my answer. This I will of course correct – Mikelgo Dec 13 '20 at 12:30