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

Jonathan Stellwag :

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.

Matt Saunders :

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.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=7004&siteId=1