Skip to content

Handling Manual Subscriptions

Sometimes we might find ourselves in situations where we need to manually subscribe to an observable stream. This might be true when the “destination” for your data isn’t actually the template. Maybe you want to send a POST request to send data to a server, and don’t need to display anything about that request in the template. Maybe you have some kind of logging service that sends analytics to some remote backend — the data is just leaving your application so, again, maybe you want to perform a manual subscription to send that data on its way.

As you will soon see, even the Signals and RxJS state management approach we will be using for the applications we will be building — the state management approach that is designed around being as reactive/declarative as practical — explicitly uses manual subscriptions to observable streams sometimes.

Manual subscriptions are best avoided, but sometimes they are simply necessary or perhaps just convenient.

How to Unsubscribe Manually

If you do subscribe manually at some point, it is important to understand how to unsubscribe safely. If you ever write .subscribe() in your code you should always make sure you unsubscribe from that stream at some point.

Technically, there are situations where you don’t have to unsubscribe — certain types of observables might just emit one value and then complete. If the complete() notifier has been called, then the stream is already unsubscribed so there would be no need to do it manually.

This is a dangerous game to play, even in situations where it seems like it might be safe to not bother with unsubscribing there might be tricky little gotchas you aren’t considering.

For example, a lot of people will say that if you subscribe to a http.get() request in Angular you don’t need to worry about unsubscribing because it just emits on value and then completes. The second part of that statement is true enough, but not unsubscribing may still cause unintended consequences.

You might have some code like this:

this.http.get('someapi').subscribe((val) => {
// do something within the currently active component
})

But what if the user leaves the page before the HTTP request finishes? If your subscriptions aren’t unsubscribed when the component is destroyed then you could have side effects of your HTTP request being triggered when the component doesn’t even exist anymore. This may or may not cause problems.

Rather than trying to reason about when you need to unsubscribe and when you don’t, it is much safer to always apply the general rule of: always unsubscribe.

I am going to show you a few different ways to handle unsubscribes that you might come across in codebases — there have been many different ways people have approached this over the years.

But, there is a new way in Angular to handle unsubscribing: the takeUntilDestroyed operator. This is almost always going to be the preferable option and it is what we will be relying on in this course.

Let’s start with the more “manual” approach first to see what exactly it is we are trying to do, and then we will cover takeUntilDestroyed and some more options. We will also look at some other ways of unsubscribing that may still be useful in certain circumstances, or you might just come across these methods in older codebases.

Store a reference to the subscription

This is the sort of “obvious” way to unsubscribe but I generally wouldn’t recommend doing this as other ways are generally more efficient. Still, I think it is important to cover since it sort of the default way to go about it.

export class HomeComponent implements OnInit, OnDestroy {
emitOnceASecondSubscription: Subscription;
ngOnInit(){
const emitOnceASecond$ = interval(1000);
this.emitOnceASecondSubscription = emitOnceASecond$.subscribe((val) => console.log(val));
}
ngOnDestroy(){
this.emitOnceASecondSubscription.unsubscribe();
}
}

The basic idea is that every time you subscribe to something, you store a reference to that subscription on a class member. Then, in the ngOnDestroy lifecycle hook, you unsubscribe from every subscription you created.

As you might be able to tell, this approach is quite awkward for multiple subscriptions and is a very imperative way to go about doing it.

Use takeUntilDestroyed

Angular recently introduced the takeUntilDestroyed operator. You can use it like this:

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
this.myStream$.pipe(takeUntilDestroyed()).subscribe((val) => console.log(val));

The basic idea is that you pipe on the takeUntilDestroyed() operator, and when your component or service is destroyed it will automatically unsubscribe from the observable for you.

There is some nuance to understand here. Using takeUntilDestroyed() will only work within an injection context — basically Angular needs to know the context of the thing being destroyed in order to trigger the unsubscribe.

Although there are a few more injection contexts than this, for most purposes this means that we can only use takeUntilDestroyed if our stream is defined when initialising a class member or within the constructor, i.e:

export class MyComponent {
http = inject(HttpClient);
myData$ = this.http.get('someapi').pipe(takeUntilDestroyed());
constructor() {
this.http.get('someapi').pipe(takeUntilDestroyed());
}
}

Using takeUntilDestroyed() in both cases above are fine because they are within the injection context. If you tried to use it inside of say ngOnInit or some other method of your class, it would not work.

There is still a way to make takeUntilDestroyed() work outside of an injection context. You can manually pass it a DestroyRef which essentially lets it know what injection context it needs to use. However, you won’t generally need to do this.

Use takeUntil

This approach is probably the most common (although this approach will slowly be replaced by the new takeUntilDestroyed), let’s take a look and then discuss it:

export class HomeComponent implements OnInit, OnDestroy {
destroy$ = new Subject();
ngOnInit(){
const emitOnceASecond$ = interval(1000);
emitOnceASecond$.pipe(
takeUntil(this.destroy$)
).subscribe((val) => console.log(val));
}
ngOnDestroy(){
this.destroy$.next();
this.destroy$.complete();
}
}

We use the takeUntil operator which will cause the stream it is piped onto to unsubscribe when the input stream to takeUntil() emits. This means that whenever we trigger .next() on our destroy$ stream, it will make emitOnceASecond$ unsubscribe.

The good thing about this approach is that we can set up our destroy$ notifier once, and then just pipe takeUntil(this.destroy$) onto any of our observables that we are subscribing to.

This is basically the same idea as takeUntilDestroyed — we are just doing it manually.

Use take

This approach is quite similar to the last approach, but rather than creating a notifier stream to cause the unsubscribe, we just take a set amount of values from the stream then automatically unsubscribe:

export class HomeComponent implements OnInit {
ngOnInit(){
const emitOnceASecond$ = interval(1000);
emitOnceASecond$.pipe(
take(1)
).subscribe((val) => console.log(val));
}
}

This method is kind of okay, and probably won’t cause any trouble, but it is not as safe as the previous two. It is also not generally applicable, as we don’t always just want a specific number of emissions from an observable stream (often we want to keep taking values until the component is destroyed).

The problem with this is that it could cause slightly unpredictable behaviour, it faces the same problem as assuming you don’t need to unsubscribe from an observable that emits one value and then completes. Imagine that the stream above makes some kind of asynchronous request that takes a few seconds to complete (just like that initial http.get() example we discussed). That means it is going to take a few seconds before we get our first value, and then it will be unsubscribed automatically because of the take() operator.