RxJS: Don’t Unsubscribe
Well… okay, just don’t unsubscribe quite so much.
I’m often enlisted to help someone debug an issue with their RxJS code or figure out how to structure an app that is composing a lot of async with RxJS. When doing so, I generally see the same thing pop up over and over again, people keeping handles to tons and tons of subscription objects. Developers will invariably make 3 HTTP requests with an Observable, and then keep 3 subscription objects that they’re going to call when some event occurs.
I can see how it would happen. People are used to using `addEventListener` N times, and then having some clean up where they have to call `removeEventListener` N times. It feels natural to do the same with subscription objects, and for the most part you’re right. But there are better ways. Keeping too many subscription objects around is a sign you’re managing your subscriptions imperatively, and not taking advantage of the power of Rx.
What imperative subscription management looks like
Take for example this make-believe component (I’ll purposely make this non-React and non-Angular and somewhat generic):
In the example above, you can see I’m manually calling `unsubscribe` on three subscription objects I’m managing myself in the `onUnmount()` method. I’m also calling `this.dataSub.unsubscribe()` when someone clicks the cancel button on line #15, and again on line #22 when the user sets the range selector above 500, which is some threshold at which I want to stop the data stream. (I don’t know why, it’s a weird component)
The ugliness here is I’m imperatively managing unsubscriptions in multiple places in this fairly trivial example.
The only real advantage to using this approach would be performance. Since you’re using fewer abstractions to get the job done, it’s likely to perform a little better. This is unlikely to have a noticeable effect in the majority of web applications however, and I don’t think it’s worth worrying about.
Alternatively, you can always combine subscriptions into a single subscription by creating a parent subscription and adding all of the others like children. But at the end of the day, you’re still doing the same thing, and you’re probably missing out.
Compose your subscription management with takeUntil
Now let’s do the same basic example, only we’ll use the `takeUntil` operator from RxJS:
Another advantage to this approach is it actually completes the observable. That means there’s a completion event that can be handled anytime you want to kill your observable. If you just call `unsubscribe` on a returned subscription object, there’s no way you’ll be notified that the unsubscription happened. However if you use `takeUntil` (or others listed below), you will be notified the observable has stopped via your completion handler.
The last advantage I’ll point out is the fact that you’re actually “wiring everything up” by calling `subscribe` in one place, this is advantageous because with discipline it becomes much, much easier to locate where you’re starting your subscriptions in your code. Remember, observables don’t do anything until you subscribe to them, so the point of subscription is an important piece of code.
There is one disadvantage here in terms of RxJS semantics, but it’s barely worth worrying about in the face of the other advantages. The semantic disadvantage is that completing an observable is a sign that the producer wants to tell the consumer it’s done, where unsubscribing is the consumer telling the producer it no longer cares about the data.
There will also be a very slight performance difference between this and just calling `unsubscribe` imperatively. However, it’s unlikely that the perf hit will be anything noticeable in the mass-majority of applications.
There are many other ways to kill a stream in a more “Rx-y” way. I’d recommend checking out the following operators at the very least:
- take(n): emits N values before stopping the observable.
- takeWhile(predicate): tests the emitted values against a predicate, if it returns `false`, it will complete.
- first(): emits the first value and completes.
- first(predicate): checks each value against a predicate function, if it returns `true`, the emits that value and completes.
Summary: Use takeUntil, takeWhile, et al.
You should probably be using operators like `takeUntil` to manage your RxJS subscriptions. As a rule of thumb, if you see two or more subscriptions being managed in a single component, you should wonder if you could be composing those better.
- more composeable
- fires a completion event when you kill your stream
- generally less code
- less to manage
- fewer actual points of subscription (because fewer calls to `subscribe`)