Hi, Habr! I present to you the translation of the article "
RxJS: Don't Unsubscribe " by Ben Lesh.
Well ... well, just do not refuse subscriptions.
I often help someone debug problems with their RxJS code, including structuring applications that carry a lot of asynchronous code. At the same time, I always see the same thing, as people keep handlers on tons of subscriptions. The developer makes 3 HTTP requests with Observable, saving 3 subscription objects that will be called when an event occurs.
')
I know why this is happening. People used to use `addEventListener` N times, and then when they are no longer needed, call` removeEventListener` N times. It will be natural to do the same with subscription objects, and for the most part you will be right. But there are better ways. Saving too many subscription objects is a sign that you are managing your subscriptions imperatively and not taking advantage of Rx.
What does imperative subscription management look like
Take, for example, this fictional component (this is not specifically React or Angular, but just a general example):
class MyGenericComponent extends SomeFrameworkComponent { updateData(data) { // - } onMount() { this.dataSub = this.getData() .subscribe(data => this.updateData(data)); const cancelBtn = this.element.querySelector('.cancel-button'); const rangeSelector = this.element.querySelector('.rangeSelector'); this.cancelSub = Observable.fromEvent(cancelBtn, 'click') .subscribe(() => { this.dataSub.unsubscribe(); }); this.rangeSub = Observable.fromEvent(rangeSelector, 'change') .map(e => e.target.value) .subscribe((value) => { if (+value > 500) { this.dataSub.unsubscribe(); } }); } onUnmount() { this.dataSub.unsubscribe(); this.cancelSub.unsubscribe(); this.rangeSub.unsubscribe(); } }
In the example above, you can see that I manually call `unsubscribe` on three subscription objects in the` onUnmount () `method. I also call `this.dataSub.unsubscribe ()` when the user clicks the cancel button on lines 15 and 22, or when he sets the range selector above 500, which is a certain threshold on which I want to stop the data flow. (I do not know why, this is just a strange component).
The disadvantage of this approach is that I manually manage subscription cancellation in several places in this rather trivial example.
The only real advantage to using this approach is performance. Since you use less abstractions to do your work, this is likely to be a little better. However, this is unlikely to have a noticeable effect in most web applications, and I do not think that this is worth worrying about.
In addition, you can always combine several subscriptions into one, creating a parent subscription and adding all the others as children. But in essence, you will do the same.
Compose subscription management with takeUntil
Now let's do the same example, but using the `takeUntil` statement from RxJS:
class MyGenericComponent extends SomeFrameworkComponent { updateData(data) { // do something framework-specific to update your component here. } onMount() { const data$ = this.getData(); const cancelBtn = this.element.querySelector('.cancel-button'); const rangeSelector = this.element.querySelector('.rangeSelector'); const cancel$ = Observable.fromEvent(cancelBtn, 'click'); const range$ = Observable.fromEvent(rangeSelector, 'change') .map(e => e.target.value); const stop$ = Observable.merge(cancel$, range$.filter(x => x > 500)) this.subscription = data$.takeUntil(stop$) .subscribe(data => this.updateData(data)); } onUnmount() { this.subscription.unsubscribe(); } }
The first thing you notice is a smaller amount of code. But this is only one advantage. Another thing that happened here is that I put events that stop the data flow into the `stop $` stream. This means that as soon as I decide that I want to add another condition to stop the flow, for example by timer, I can simply add a new watched object to `stop $`. The next obvious thing is that I have only one subscription object, which I manage imperatively. You can’t change it, because here functional programming intersects with the object-oriented world. Javascript is an imperative language and we have to accept the rest of the world in some sense half.
Another advantage of this approach is that it actually completes the observed object. This means that a termination event will occur that can be processed at any time. If you simply call unsubscribe on the returned subscription object, you will not be notified that the subscription has been canceled. However, if you use `takeUntil` (or other operators listed below), you will be informed through the completion handler that the observable object has stopped.
The last advantage I’m talking about is that you actually “plug everything in”, causing a subscription in one place, which is good, because with discipline it becomes much easier to find where you start a subscription in your code. Remember that observable objects do nothing until you subscribe to them, so the subscription point is an important part of the code.
True, there is one drawback in terms of the semantics of RxJS, but it is hardly worth worrying about other advantages. The semantic flaw is that the completion of the observed object is a sign that the producer wants to tell the consumer that the job is done, while unsubscribing is the consumer telling the manufacturer that he no longer needs the data.
There will also be a very small performance difference between this and the simple imperative call of `unsubscribe`. However, it is unlikely that it will be noticeable in most applications.
Other operators
There are many other ways to stop the flow in “Rx-way”. I would recommend looking at the following operators at a minimum:
take (n): takes N values ​​before stopping the observed.
takeWhile (predicate): checks the values ​​passed through itself on the predicate; if it returns false, the stream will be terminated.
first (): skips the first value and exits.
first (predicate): checks each value for a predicate function; if it returns true, the thread skips the value and ends.
Summary: Use takeUntil, takeWhile, etc.
You should probably use operators such as `takeUntil` to manage subscriptions in RxJS. As a rule, if you see that there are two or more subscriptions in one component, you should ask yourself if you can define them better:
- first, so advanced
- triggers a completion event when you kill your thread
- usually this is less code
- it becomes easier to manage
- fewer actual subscription points (because fewer calls to `subscribe`)
Want to know more about RxJS from the author? Come on rxworkshop.com!