Dino Dujmovic
Posted on April 14, 2023
There are many posts and videos out there discussing how to unsubscribe from Observables in Angular, so I was thinking - why shouldn't I write another one?
Introduction
Here are some important things to keep in mind when working with Observables in Angular:
- In order to retrieve the value from an Observable, we need to subscribe to it.
- Unsubscription does not happen automatically. We are responsible for unsubscribing from the Observable when we no longer need it.
- We need to unsubscribe from our Observables because if we don't, they can continue to emit values even after the component is no longer present in the DOM (i.e., destroyed). This can cause memory leaks, as the subscription and associated resources will still be active in memory, even though they are no longer needed.
Async Pipe
The most safe way to approach subscribing and rendering the data in Angular is using async pipe.
When we use the async pipe in our template to bind to an Observable, Angular takes care of subscribing to and unsubscribing from the Observable for us. This means that we don't need to manually subscribe to the Observable in our component code, and we don't need to worry about manually unsubscribing and it also simplifies the code quite a bit.
Note: For examples below MovieService makes HTTP call with Angular's HttpClient module that returns observable
@Component({
template: `
<div class="movies">
<div *ngIf="popularMovies$ | async as movies" class="popular-movies">
<div *ngFor="let movie of movies">{{ movie.title }} </div>
</div>
<div *ngIf="trendingMovies$ | async as movies" class="trending-movies">
<div *ngFor="let movie of movies">{{ movie.title }} </div>
</div>
</div>
`
})
class MovieComponent implements OnInit, OnDestroy {
popularMovies$: Observable<IMovie[]>;
trendingMovies$: Observable<IMovie[]>;
constructor(private movieService: MovieService) {
this.popularMovies$ = movieService.getPopularMovies()
this.trendingMovies$ = movieService.getTrendingMovies()
}
}
Given the benefits that it provides, I believe that the async pipe should be used wherever possible in Angular applications, but there may be situations where you would need to subscribe/unsubscribe manually and here is how:
1) Basic approach using unsubscribe
@Component({...})
class MovieComponent implements OnInit, OnDestroy {
popularMoviesSub: Subscription;
trendingMoviesSub: Subscription;
constructor(private movieService: MovieService) {
this.popularMoviesSub = movieService.getPopularMovies().subscribe();
this.trendingMoviesSub = movieService.getTrendingMovies().subscribe();
}
ngOnDestroy() {
this.popularMoviesSub.unsubscribe()
this.trendingMoviesSub.unsubscribe()
}
}
The problem with this approach is that we could be creating many Subscription properties and we may also easily forget to unsubscribe from all of them.
2) RxJS takeUntil
@Component({...})
export class MovieComponent implements OnInit, OnDestroy {
destroy$ = new Subject<boolean>();
constructor(private movieService: MovieService) {
movieService.getPopularMovies()
.pipe(takeUntil(this.destroy$))
.subscribe()
movieService.getTrendingMovies()
.pipe(takeUntil(this.destroy$))
.subscribe()
}
ngOnDestroy() {
this.destroy$.next();
}
}
This is considered to be the most correct reactive programming approach.
However, in my humble opinion it can result in code that is less aesthetically pleasing and harder to understand for many.
3) Simple solution I like to use
My personal belief is that simplicity and readability are critical to writing high-quality code. It's important to strive for code that is clear, concise, and easy to follow, regardless of whether the intended audience is a beginner or an advanced developer.
@Component({...})
export class MovieComponent implements OnInit, OnDestroy {
private subs = new ManualSubs();
constructor(private movieService: MovieService) {
this.subs.add = movieService.getPopularMovies().subscribe();
this.subs.add = movieService.getTrendingMovies().subscribe();
}
ngOnDestroy() {
this.subs.unsubscribe();
}
}
Here is how ManualSubs class looks like:
// manual-subs.ts
export class ManualSubs {
private subs: Subscription[] = [];
private isValidSubscription(sub: Subscription) {
return sub && typeof sub.unsubscribe === "function";
}
set add(sub: Subscription) {
if (this.isValidSubscription(sub)) {
this.subs.push(sub);
}
}
public unsubscribe() {
this.subs.forEach((sub: Subscription) => this.isValidSubscription(sub) && sub.unsubscribe());
this.subs = [];
}
}
Never forget to test!
// manual-subs.spec.ts
describe("ManualSubs", () => {
let manualSubs: ManualSubs;
let sub1: Subscription;
let sub2: Subscription;
beforeEach(() => {
manualSubs = new ManualSubs();
sub1 = jasmine.createSpyObj("Subscription", ["unsubscribe"]);
sub2 = jasmine.createSpyObj("Subscription", ["unsubscribe"]);
});
it("should unsubscribe from all subscriptions", () => {
manualSubs.add = sub1;
manualSubs.add = sub2;
manualSubs.unsubscribe();
expect(sub1.unsubscribe).toHaveBeenCalledTimes(1);
expect(sub2.unsubscribe).toHaveBeenCalledTimes(1);
});
});
Existing libraries using this approach:
However, it's worth considering whether you truly need an external npm module for a relatively small amount of code, especially when you could easily write, manage, expand, and use your own naming conventions.
Summary
The approaches discussed are some of the most straightforward and commonly used methods for managing subscriptions.
However, there are also other more "advanced" techniques available that can simplify subscription management and reduce boilerplate code, such as using Angular decorators.
As always we can implement those ourselves, but there are some nice solutions out there if some would prefer.
Posted on April 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.