Last week I answered an RxJS question where I got into a discussion with another community member about: "Should I create a subscription for every specific side effect or should I try to minimize subscriptions in general?" I want to know what methology to use in terms of a full reactive application approach or when to switch from one to another. This will help me and maybe others to avoid unecesarry discussions.
Setup info
What is a side effect (Angular sample)
value$ | async
)@Output event = event$
)Exemplary usecase:
foo: () => void; bar: (arg: any) => void
http$: Observable<any>; click$: Observable<void>
foo
is called after http$
has emitted and needs no valuebar
is called after click$
emits, but needs the current value of http$
Case: Create a subscription for every specific side effect
const foo$ = http$.pipe(
mapTo(void 0)
);
const bar$ = http$.pipe(
switchMap(httpValue => click$.pipe(
mapTo(httpValue)
)
);
foo$.subscribe(foo);
bar$.subscribe(bar);
Case: Minimize subscriptions in general
http$.pipe(
tap(() => foo()),
switchMap(httpValue => click$.pipe(
mapTo(httpValue )
)
).subscribe(bar);
My own opinion in short
I can understand the fact that subscriptions make Rx landscapes more complex at first, because you have to think about how subscribers should affect the pipe or not for instance (share your observable or not). But the more you separate your code (the more you focus: what happens when) the easier it is to maintain (test, debug, update) your code in the future. With that in mind I always create a single observable source and a single subscription for any side effect in my code. If two or more side effects I have are triggered by the exact same source observable, then I share my observable and subscribe for each side effect individually, because it can have different lifecycles.
RxJS is a valuable resource for managing asynchronous operations and should be used to simplify your code (including reducing the number of subscriptions) where possible. Equally, an observable should not automatically be followed by a subscription to that observable, if RxJS provides a solution which can reduce the overall number of subscriptions in your application.
However, there are situations where it may be beneficial to create a subscription that is not strictly 'necessary':
An example exception - reuse of observables in a single template
Looking at your first example:
// Component:
this.value$ = this.store$.pipe(select(selectValue));
// Template:
<div>{{value$ | async}}</div>
If value$ is only used once in a template, I'd take advantage of the async pipe and its benefits for code economy and automatic unsubscription. However as per this answer, multiple references to the same async variable in a template should be avoided, e.g:
// It works, but don't do this...
<ul *ngIf="value$ | async">
<li *ngFor="let val of value$ | async">{{val}}</li>
</ul>
In this situation, I would instead create a separate subscription and use this to update a non-async variable in my component:
// Component
valueSub: Subscription;
value: number[];
ngOnInit() {
this.valueSub = this.store$.pipe(select(selectValue)).subscribe(response => this.value = response);
}
ngOnDestroy() {
this.valueSub.unsubscribe();
}
// Template
<ul *ngIf="value">
<li *ngFor="let val of value">{{val}}</li>
</ul>
Technically, it's possible to achieve the same result without valueSub
, but the requirements of the application mean this is the right choice.
Considering the role and lifespan of an observable before deciding whether to subscribe
If two or more observables are only of use when taken together, the appropriate RxJS operators should be used to combine them into a single subscription.
Similarly, if first() is being used to filter out all but the first emission of an observable, I think there is greater reason to be economical with your code and avoid 'extra' subscriptions, than for an observable that has an ongoing role in the session.
Where any of the individual observables are useful independently of the others, the flexibility and clarity of separate subscription(s) may be worth considering. But as per my initial statement, a subscription should not be automatically created for every observable, unless there is a clear reason to do so.
Regarding Unsubscriptions:
A point against additional subscriptions is that more unsubscriptions are required. As you've said, we would like assume that all necessary unsubscriptions are applied onDestroy, but real life doesn't always go that smoothly! Again, RxJS provides useful tools (e.g. first()) to streamline this process, which simplifies code and reduces the potential for memory leaks. This article provides relevant further information and examples, which may be of value.
Personal preference / verbosity vs. terseness:
Do take your own preferences into account. I don't want to stray towards a general discussion about code verbosity, but the aim should be to find the right balance between too much 'noise' and making your code overly cryptic. This might be worth a look.
if optimizing subscriptions is your endgame, why not go to the logical extreme and just follow this general pattern:
const obs1$ = src1$.pipe(tap(effect1))
const obs2$ = src2$pipe(tap(effect2))
merge(obs1$, obs2$).subscribe()
Exclusively executing side effects in tap and activating with merge means you only ever have one subscription.
One reason to not do this is that you’re neutering much of what makes RxJS useful. Which is the ability to compose observable streams, and subscribe / unsubscribe from streams as required.
I would argue that your observables should be logically composed, and not polluted or confused in the name of reducing subscriptions. Should the foo effect logically be coupled with the bar effect? Does one necessitate the other? Will I ever possibly not to want trigger foo when http$ emits? Am I creating unnecessary coupling between unrelated functions? These are all reasons to avoid putting them in one stream.
This is all not even considering error handling which is easier to manage with multiple subscriptions IMO
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With