tap() vs subscribe() to set a class property
Asked Answered
M

4

63

I am very new to rxjs and was just wondering is it ok to setup a class property by piping the stream and tapping it, or it should i do it in the subscribe. To me either way works, just wonder if it is ok to do it as I see fit to my eyes or there is something I am unaware of.

Typescript code demonstrating both ways:

export class ViewComponent implements OnInit {

  applicant = {};

  constructor(public route: ActivatedRoute, private store: Store<any>) {}

  ngOnInit() {
    this.route.paramMap.pipe(
      switchMap(params => this.store.select(state => state.applicants.entities[params.get('id')])),
      tap(applicant => this.applicant = applicant)
    ).subscribe();
  }
}

vs

export class ViewComponent implements OnInit {

  applicant = {};

  constructor(public route: ActivatedRoute, private store: Store<any>) {}

  ngOnInit() {
    this.route.paramMap.pipe(
      switchMap(params => this.store.select(state => state.applicants.entities[params.get('id')]))
    ).subscribe(applicant => this.applicant = applicant);
  }
}
Minimize answered 9/3, 2018 at 0:27 Comment(9)
Interesting question. I've always done the second one when working with route parameters but not for any particular reason.Reproduce
I've voted to close this as primarily opinion-based, but I'd choose number two. There is no difference in behaviour, but using tap means you'll be importing more code that you actually need.Glycolysis
But isn't it tap one of the common operators? I think you would end up importing it somewhere anyway and after the bundling it wont make any difference in size. Doing everything in the pipe is bit more understandable in my opinion, just visually.Minimize
With Rxjs 5.5+ imports, you won't be using a global tap import / adding it to the Observable prototype. It does become extra code in your program.Oversee
Really?! So all this tree shaking, webpacking, commonchunking and etc will result in having tap ten times duplicated? It is strange to me.Minimize
Not super sure about the details with tree shaking and common chunking and stuff :/. Here's a link with the "Why" behind it: github.com/ReactiveX/rxjs/blob/master/doc/…Oversee
The link is about the dot-chained operators.Minimize
I was linking it because it mentions the difference in tree shaking between the old approach and the new. I think what @Glycolysis meant about importing more code than you need is that anything you can do with tap can be done in subscribe, so using tap anywhere is likely "more code than you actually need"Oversee
You're already importing RxJS. Likely a lot of other things if you're using this with Angular. Importing one more operator, especially something trivial like tap, is not going to result in any visible impact in code size, or performance.Ecclesiolatry
C
43

tap is useful when you have the observable separated from its subscriber. If you have a class that exposes an observable, you can use tap to implement side effects that this class needs to be executed when someone is listening to the observable.

On the other side, when you subscribe to it from another class, you can implement side effects from the subscriber's point of view, using subscribe.

Class with the observable:

public dummyObservable: Observable<number> = from([1, 2, 3, 4, 5]).pipe(
  // Side effects, executed every time I emit a value
  // I don't know which side effects implements who subscribes to me
  tap(n => console.log("I'm emitting this value:", n))
);

Class with the subscription:

ngOnInit(): void {
  this.dummyService.dummyObservable.subscribe(
    // Side effects, executed every time I receive a value
    // I don't know which side effects implements the observable
    data => console.log("I'm receiving this value: ", data)
  );
}
Coax answered 15/6, 2018 at 20:20 Comment(0)
O
55

Edit: Ignore this answer!

Here is a good answer: https://mcmap.net/q/302234/-tap-vs-subscribe-to-set-a-class-property

See JMD's comment below for more context!


Good question. In the source code for the tap operator, this comment pretty much sums it up:

This operator is useful for debugging your Observables for the correct values or performing other side effects.
Note: this is different to a subscribe on the Observable. If the Observable returned by do is not subscribed, the side effects specified by the Observer will never happen. do therefore simply spies on existing execution, it does not trigger an execution to happen like subscribe does.

Any side effect you can run in a tap can probably also be put in the subscribe block. The subscribe indicates your intent to actively use the source value since it's saying "when this observable emits, I want to save it's value in the applicants variable". The tap operator is mostly there for debugging, but it can be used to run side effects.

In general, favor the subscribe block for running side effects, use tap for debugging, but be aware that tap can do more if you need it to.

Oversee answered 9/3, 2018 at 2:34 Comment(5)
I use tap when I need to add mandatory side effect to an observable.They will always activate regardless of how they are subscribedCrosson
I could not disagree more. All observables should set side effects through taps so that they can be returned and further modified by additional pipes. Once you force a subscribe you lose all ability to control the observable further. IMHO, subscribes should always be empty.Barramunda
I could see this being used in conjunction with the angular AsyncPipe. The async pipe is designed to enable declarative logic without worrying about subscribing/unsubscribing to the streams. The async pipe handles the subscription process for you. So, let's say you have a model used in a form on the template vm$. This model is an observable, and will be handled by the async pipe, you need not subscribe. However, when the form is submitted, you want to use a snapshot of the current model. Allowing a tap operator to update a fellow vm property seems like it would be useful and applicable.Boult
I agree with the general idea of using subscribe as the default place for running side effects, however tap is sometimes needed because the end result of an observable might no longer contain the value you wanted for your side effect. For example if one of the operators was reduce or map the values you were looking for might be gone.Cara
I think this answer is outdated. If you go to rxjs.dev/api/operators/tap it says the following Tap is designed to allow the developer a designated place to perform side effects. Even if you go the link given in the answer above it says the same thingAutunite
C
43

tap is useful when you have the observable separated from its subscriber. If you have a class that exposes an observable, you can use tap to implement side effects that this class needs to be executed when someone is listening to the observable.

On the other side, when you subscribe to it from another class, you can implement side effects from the subscriber's point of view, using subscribe.

Class with the observable:

public dummyObservable: Observable<number> = from([1, 2, 3, 4, 5]).pipe(
  // Side effects, executed every time I emit a value
  // I don't know which side effects implements who subscribes to me
  tap(n => console.log("I'm emitting this value:", n))
);

Class with the subscription:

ngOnInit(): void {
  this.dummyService.dummyObservable.subscribe(
    // Side effects, executed every time I receive a value
    // I don't know which side effects implements the observable
    data => console.log("I'm receiving this value: ", data)
  );
}
Coax answered 15/6, 2018 at 20:20 Comment(0)
M
13

Michael Hladky suggests that you put all side effects in the tap operator and he explains why here.

I think it's generally a good idea to do so, because as Michael says, you can then merge many observables together and create one single subscription for all of them.

I don't know if this improves performance or not, but it definitely makes it easier when you want to unsubscribe to only have one subscription. Unsubscribing is something you always should do to avoid possible memory leaks or other strange behaviors.

Another gain from this approach is that you then easily can pause, resume or complete a group of observables by piping it through operators such as filter or takeWhile, or by switchmapping it through another observable like this:

const allMergedObservables$ = merge(obs1, obs2, obs3);
const play$ = new Subject();

play$.asObservable().pipe(
    switchMap(bool => bool ? allMergedObservables$ : EMPTY)
).subscribe();

// Putting 'true' into the play stream activates allMergedObservables$. 
play$.next(true);

// Something happens that makes you want to pause the application,
// for instance the user opens the print dialog box,
// so you issue 'false' in the play stream which in turn stops the
// inner subscription of allMergedObservables$:
play$.next(false);

However, it's up to you and whatever programming style you prefer.

Mcbride answered 23/10, 2019 at 18:55 Comment(0)
P
5

With AsyncPipe and NgrxPushPipe

For those who would like to use async pipe or ngrxPush pipe then you have no option but to use tap operator

In the example above we can have something like

export class ViewComponent implements OnInit {
  
  applicant = {};
  applicant$ = this.route.paramMap.pipe(
      switchMap(params => this.store.select(state => state.applicants.entities[params.get('id')])),
      tap(applicant => this.applicant = applicant)
    )
  constructor(public route: ActivatedRoute, private store: Store<any>) {}

  ngOnInit() { }
}

and in the html

Using AsyncPipe

<ng-container *ngIf='applicant$ | async'>

   ...Some Html code here

</ng-container>

Using NgrxPushPipe (remember this only works if you import { ReactiveComponent } from '@ngrx/component')

<ng-container *ngIf='applicant$ | ngrxPush'>
 
...Some Html code here

</ng-container>

Additions

This above two pipes help in improving code maintainability and reduces/removes risk of memory leaks due to unsubscribed observables

The above code can be simplified to

TS FILE

export class ViewComponent {
 
  applicant$ = this.route.paramMap.pipe(
      switchMap(params => this.store.select(state => 
        state.applicants.entities[params.get('id')])
      )
    )
  constructor(public route: ActivatedRoute, private store: Store<any>) {}

}

HTML File

Using AsyncPipe

<ng-container *ngIf='applicant$ | async as applicant'>

   ...Some Html code here

</ng-container>

Using NgrxPushPipe

<ng-container *ngIf='applicant$ | ngrxPush as applicant'>

   ...Some Html code here

</ng-container>

Using NgrxPushPipe

<ng-container *ngIf='applicant$ | ngrxPush as applicant'>

   ...Some Html code here

</ng-container>

Using ngrxLet Structural Directive (also from '@ngrx/component')

<ng-container *ngrxLet='applicant$; let applicant'>

   ...Some Html code here

</ng-container>
Poff answered 15/4, 2021 at 12:5 Comment(1)
Can you please provide your insight in this #67131132Malaya

© 2022 - 2024 — McMap. All rights reserved.