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.