RxJS: Don’t takeUntil

Andrew Crites
ITNEXT
Published in
5 min readJun 17, 2019

--

This is a bit of a followup to Ben Lesh’s excellent article RxJS: Don’t Unsubscribe. I’m going for a catchy title here that mirrors his. I think you should use takeUntil when you have a good reason to do so, and subscription management is a good reason. Just like Ben says

Well… okay, just don’t unsubscribe quite so much.

I would say, make sure you’re using takeUntil properly for managing your Observables as well. On a related note, you may want to check out Nicholas Jamieson’s article RxJS: Avoiding takeUntil Leaks.

This article will detail some issues I ran into with naive use of takeUntil in an Ionic (Angular) project. Hopefully, this can help some of you avoid these pitfalls up front! This app is an Ionic v3 app, so we’re still using some of the older lifecycle hooks. Ionic v4 recommends using Angular’s built in hooks in some cases. See: https://ionicframework.com/docs/angular/lifecycle

Subscription Management with takeUntil

Let’s say you have a component with more than one Observable. You should ask yourself a couple of questions up front:

  1. Can I use the | async pipe to manage Observable subscriptions in the component for me?
  2. Is there any sane way to merge the Observables to manage a single subscription?

Even if you answer “yes” to number 2, you might still want to use takeUntil for consistency’s sake. For example:

@Component()
export class ObservingPage {
users = this.store.selectObservable('users');
userList = [];
unsubscriber = new Subject();
ionViewDidLoad() {
this.users.pipe(takeUntil(unsubscriber)).subscribe(users =>
this.userList = users,
);
}
ionViewWillUnload() {
this.unsubscriber.next();
this.unsubscriber.complete();
}
}

In this case it would be favorable to use the async pipe with users, but you may have some teammates that wrote code when they were not familiar with the async pipe or didn’t want to use it. Perhaps there is a better example where you had to create a manual subscription. Either way, the point here is that when the view unloads, the users Observable will complete because we’re emitting to unsubscriber and using takeUntil.

The app I was working on with a large team had many memory leak issues caused by improperly unsubscribed Observables. Coming in with no judgment, we followed the team’s suggestion to use takeUntil when unloading components to fix these memory leaks. For the most part, this worked great, and it’s a viable solution.

Managing subscriptions with different lifecycles

This solution is all well and good, but Ionic will keep pages in memory even if you navigate away from them. This leads to some subscription callbacks being called in the background of a page which may be unnecessary or undesirable. No matter… we can use the WillEnter / WillLeave lifecycle hooks instead.

@Component()
export class OnlyWhenEnteredPage {
users = this.store.selectObservable('users');
userList = [];
unsubscriber = new Subject();
ionViewWillEnter() {
this.users.pipe(takeUntil(unsubscriber)).subscribe(users =>
this.userList = users,
)
}
ionViewWillLeave() {
this.unsubscriber.next();
this.unsubscriber.complete();
}
}

Hopefully, this looks innocent enough to some of you as it did to me. Perhaps others are screaming at the glaringly obvious (in retrospect) issue here: unsubscriber is created when the page loads, but we complete it when the page leaves. This means that every time we re-enter the page, anything using takeUntil(unsubscriber) will complete immediately since we’ve already emitted to it.

If you were blindsided by a similar issue and started questioning your understanding of Observable creation and completion, I know exactly how you feel since I went through that exact thing. The solution here is relatively simple: create your unsubscriber Subject manager on enter too.

@Component()
export class OnlyWhenEnteredPage {
users = this.store.selectObservable('users');
userList = [];
unsubscriber: Subject<void>();
ionViewWillEnter() {
this.unsubscriber = new Subject();
this.users.pipe(takeUntil(unsubscriber)).subscribe(users =>
this.userList = users,
);
}
ionViewWillLeave() {
this.unsubscriber.next();
this.unsubscriber.complete();
}
}

A more robust approach?

Using takeUntil like this has always rubbed me the wrong way for some reason. I think it might have to do with calling both .next and .complete, (you _technically_ don’t have to do .complete, but not doing so could cause a memory leak if I’m not mistaken).

Ward Bell’s simple subsink tool seems like a good approach. This essentially just does a manual unsubscribe of a list of subscriptions you’re creating when you unsubscribe the sink, but you don’t have to worry about managing and completing a separate Subject which is one more Observable to deal with.

import { SubSink } from 'subsink';@Component()
export class WhenEnteredAndLoadedPage {
users = this.store.selectObservable('users');
invitations = this.store.selectObservable('invitations')
userList = [];
invitationList = [];
pageEnterSubs = new SubSink();
pageLoadSubs = new SubSink();
ionViewDidLoad() {
this.pageLoadSubs.sink = this.invitations.subscribe(inv =>
this.invitationList = inv,
);
}
ionViewWillEnter() {
this.pageEnterSubs.sink = this.users.subscribe(users =>
this.userList = users,
);
}
ionViewWillLeave() {
this.pageEnterSubs.unsubscribe();
}
ionViewWillUnload() {
this.pageLoadSubs.unsubscribe();
}
}

Each of the examples above uses only one subscription for each lifecycle hook, but you could use as many as you like. subsink doesn’t seem to be particularly popular, but it seems like a good way to manage multiple subscriptions to me in this context. If you don’t want to use it, you can use takeUntil and two subjects in a similar way, it’s just that you’d have to create one of the Subjects in ionViewWillEnter instead of the constructor.

Of course, the async pipe always beckons.

A reader also pointed out to me that this functionality is built into RxJS Subscriptions as well, so if you would rather not use subsink, you can simply use new Subscription with .add for similar functionality. See: https://rxjs.dev/api/index/class/Subscription#add and https://blog.angularindepth.com/rxjs-composing-subscriptions-b53ab22f1fd5.

Lessons Learned

RxJS is very powerful, but to that end it’s easy to make mistakes. It’s important to think about what you’re doing whenever you use an RxJS operator and ask how this will affect the stream and whether the stream will be completed appropriately. It’s very easy to make a mistake of never completing an Observable and creating a memory leak. As I’ve discovered, it’s also kind of easy to complete the Observable too early, and it will not emit when you would expect it to do so.

When working with lifecycle hooks, it’s also important to remember to mirror operations in corresponding hooks. You could think “this Subject is completed in WillLeave, so it needs to be created in WillEnter.”

--

--