Cover photo by Tim Swaan on Unsplash.
This article explains how to manage subscriptions in Angular components without repeating the same teardown logic in each component.
Common Ways
There are two common ways to manage RxJS subscriptions in Angular components to prevent memory leaks:
Using Subscription
@Component({
selector: 'interval',
templateUrl: './interval.component.html',
})
export class IntervalComponent implements OnInit, OnDestroy {
// initialize `Subscription` object
private readonly subscriptions = new Subscription();
ngOnInit(): void {
// add all subscriptions to it
this.subscriptions.add(
interval(1000)
.pipe(map(i => `== ${i} ==`))
.subscribe(console.log)
);
this.subscriptions.add(
interval(2000)
.pipe(map(i => `=== ${i} ===`))
.subscribe(console.log)
);
}
ngOnDestroy(): void {
// unsubscribe from all added subscriptions
// when component is destroyed
this.subscriptions.unsubscribe();
}
}
Using Destroy Subject
@Component({
selector: 'interval',
templateUrl: './interval.component.html',
})
export class IntervalComponent implements OnInit, OnDestroy {
// initialize destroy subject
private readonly destroySubject$ = new Subject<void>();
ngOnInit(): void {
interval(1000)
.pipe(
map(i => `== ${i} ==`),
// unsubscribe when destroy subject emits an event
takeUntil(this.destroySubject$)
)
.subscribe(console.log);
interval(2000)
.pipe(
map(i => `=== ${i} ===`),
takeUntil(this.destroySubject$)
)
.subscribe(console.log);
}
ngOnDestroy(): void {
// emit destroy event when component is destroyed
this.destroySubject$.next();
}
}
Both solutions have the same drawback: We have to initialize the additional property, and add teardown logic to the ngOnDestroy
method. However, there is a better way to manage subscriptions in Angular components.
Solution
We can put the teardown logic in a single place by creating Destroy
class that extends the Observable
class and implements the OnDestroy
interface:
@Injectable()
export class Destroy extends Observable<void> implements OnDestroy {
// initialize destroy subject
private readonly destroySubject$ = new ReplaySubject<void>(1);
constructor() {
// emit destroy event to all subscribers when destroy subject emits
super(subscriber => this.destroySubject$.subscribe(subscriber));
}
ngOnDestroy(): void {
// emit destroy event when component that injects
// `Destroy` provider is destroyed
this.destroySubject$.next();
this.destroySubject$.complete();
}
}
Then, we can provide Destroy
at the component level and inject it through the constructor:
@Component({
// provide `Destroy` at the component level
viewProviders: [Destroy]
})
export class IntervalComponent implements OnInit {
// inject it through the constructor
constructor(private readonly destroy$: Destroy) {}
ngOnInit(): void {
interval(1000)
.pipe(
map(i => `== ${i} ==`),
// unsubscribe when `destroy$` Observable emits an event
takeUntil(this.destroy$)
)
.subscribe(console.log);
}
}
When a provider is provided at the component level, it will be tied to the component lifecycle which allows us to use the ngOnDestroy
lifecycle method within it. Therefore, the ngOnDestroy
method of the Destroy
provider will be called when the IntervalComponent
is destroyed.
Conclusion
In general, manual (un)subscriptions in Angular components should be avoided. If you need to perform a side effect at the component level, you can do so using the @ngrx/component-store
effects, and let ComponentStore
take care to prevent memory leaks. However, if you prefer to manage the side effects in the components, consider using the Destroy
provider to avoid repeating the same teardown logic in each component.
Top comments (14)
Great article! 👌
I have written a similar article that describes 5 ways to unsubscribe observables and I think you just proposed the 6th! 🚀
[4+1 ways] How to Unsubscribe from Observables in Angular like a 😎
Nikos Anifantis ・ May 26 ・ 7 min read
Good one Nikos !!
Many many thanks Madhu!
Thanks Nikos!
It’s a good idea to destroy observables using a helper class but not recommend to do this way as it causes each class or component to inherit the helper class to just destroy a subscription. If you expect a observer and want to kill it just Do pipe(take(1)) and if you expect a promise like user profile api request use.toPromise() instead of .subscribe(). I say this from experience using to helper method just cause extra code that’s not need. But great Article
I quite like npmjs.com/package/@ngneat/until-de... . I think it's worth a look. I agree though that ComponentStore is fantastic :)
Sorry I skimmed and missed the subtleties of your approach, that's really nice :)
Thank you :)
Wow, this is clever. This pattern will reduce a lot of boilerplate from my code, thanks!
Nice one !!
Cool! How about the async pipe approach also?
Yes,
async
is always a great choice for displaying Observable results on a template :)Thanks Ankita! I will :)