When should I create a new Subscription for a specific side effect?
Asked Answered
Y

2

9

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

  • All examples are in TypeScript
  • For better focus on question no usage of lifecycles/constructors for subscriptions and to keep in framework unrelated
    • Imagine: Subscriptions are added in constructor/lifecycle init
    • Imagine: Unsubscribe is done in lifecycle destroy

What is a side effect (Angular sample)

  • Update/Input in the UI (e.g. value$ | async)
  • Output/Upstream of a component (e.g. @Output event = event$)
  • Interacton between different services on different hierarchies

Exemplary usecase:

  • Two functions: foo: () => void; bar: (arg: any) => void
  • Two source observables: http$: Observable<any>; click$: Observable<void>
  • foo is called after http$ has emitted and needs no value
  • bar 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.

Young answered 17/2, 2020 at 8:13 Comment(0)
P
6

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.

Prepotency answered 21/2, 2020 at 2:12 Comment(14)
First thank you for your detailed answer! Regarding #1: from my point of view the async pipe is also a subscriptions/side effect, just that it's masked inside an directive. Regarding #2 can you please add some code sample, I don't get the point what you are telling me exactly. Regarding Unsubscriptions: I have never had it before that subscriptions need to be unsubscribed at any other place than the ngOnDestroy. First() does not really manage the unsubscribe for you by default: 0 emits = subscription open, although component is destroyed.Young
If it's unclear what I exactly want to know, I can adjust the question. Maybe check the linked question above, if you did not already, and read the comments under the accepcted answer. This was the main motivation for me to ask this question here.Young
No problem - thanks for the comments! Point 1 is intended to illustrate that there are situations where a subscription may be beneficial, even if it is not strictly necessary; multiple use of an async pipe in a template was just the example I picked, but I'm sure there are others.Prepotency
Point 2 was really just about considering role of each observable when deciding whether to also set up a subscription, rather than automatically following every observable with a subscription. On this point, I'd suggest looking at medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87, which says: "Keeping too many subscription objects around is a sign you’re managing your subscriptions imperatively, and not taking advantage of the power of Rx."Prepotency
Interestingly, this article also says "The only real advantage to using this approach (more subscriptions) would be performance.", similar to Point 1 in my answer.Prepotency
RE unsubscriptions, these are often applied in ngOnDestroy, but if your code unsubscribes (where possible) before the component is destroyed, it's not necessary to then also unsubscribe onDestroy.Prepotency
You're correct that first() only unsubscribes automatically once the first value has been emitted, but the prevailing view is that if you application is working correctly, it does manage unsubscription for you - see #49684100Prepotency
You could obviously use first and then also include a backup unsubscription onDestroy to manage the scenario you describe, but I think this would be excessive and if you anticipate first() never receiving a value, it's probably the wrong operator to use.Prepotency
Hi I understand your points now. You and the other answer hit the point of my question very good. Thank you for the high effort. I will accept your answer because of the effort. One point I have to mention about the first(). As long as subscriptions run, they can keep nodes in the dom, because the callbacks of the subscribes refer to the component (like every callback does). That being said means that every long running subscription can cause services/components to still keep being as a node active. That causes memory leaks.Young
Appreciated thanks @JonathanStellwag. Thanks also for the info RE callbacks - in your apps, do you explicitly unsubscribe from every subscription (even where first() is used, for example), to prevent this?Prepotency
Yes I do. In the current project we minimized around 30% of all nodes hanging around by just unsubscribing always.Young
My preference for unsubscribing is takeUntil in combination with a function that is called from ngOnDestroy. It's a one liner that adds this to the pipe: takeUntil(componentDestroyed(this)). https://mcmap.net/q/1317133/-angular-subscribe-elegantKaceykachina
Ah that's really neat - thanks @KurtHamilton. Much more elegant than a list of unsubs in onDestroyPrepotency
If all subscriptions are added to a subscription const subs = new Subscription() you can add each subscription to it subs.add(obs$.subscribe(...)) and unsubscribe in a oneliner: ngOnDestroy() { subs.unsubscribe() }Young
G
2

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

Geld answered 21/2, 2020 at 23:1 Comment(1)
Thank you for your answer. I am sorry that I only can accept one answer. Your answer is the same right as the written one by @Matt Saunders. Its just another point of view. Because of Matt's effort I gave him the accept. I hope you can forgive me :)Young

© 2022 - 2024 — McMap. All rights reserved.