A Deep Dive into Angular's NgOnDestroy
This article is a continuation of an Angular Hot Tip tweet that I sent out earlier this week. It became widely popular and generated quite a discussion. The concepts explored in this article reflect that discussion, so you should probably take some time and go check it out here:
There was a 'Not Found' error fetching URL: 'https://twitter.com/wesgrimes/status/1110603853089701888'
As an extension of the above mentioned tweet, we will discuss limitations with how and when ngOnDestroy
is called. We will also discuss ways to overcome those limitations. If you are new to Angular, or new to lifecycle methods in Angular, then I suggest you check out the official docs here.
NPM Package Versions
For context, this article assumes you are using the following npm
package.json
versions:
@angular/*
: 7.2.9
A Brief Primer On NgOnDestroy
Before we dig too deep, let’s take a few minutes and review ngOnDestroy
.
NgOnDestroy is a lifecycle method that can be added by implementing OnDestroy
on the class and adding a new class method named ngOnDestroy
. It’s primary purpose according to the Angular Docs is to “Cleanup just before Angular destroys the directive/component. Unsubscribe Observables and detach event handlers to avoid memory leaks. Called just before Angular destroys the directive/component.”
A Leaky MyValueComponent
Let’s imagine that we have a component named MyValueComponent
that subscribes to a value from MyService
in the ngOnInit
method:
import { Component, OnInit } from '@angular/core';
import { MyService } from './my.service';
@Component({
selector: 'app-my-value',
templateUrl: './my-value.component.html',
styleUrls: [ './my-value.component.css' ]
})
export class MyValueComponent implements OnInit {
myValue: string;
constructor(private myService: MyService) {}
ngOnInit() {
this.myService.getValue().subscribe(value => this.myValue = value);
}
}
If this component is created and destroyed multiple times in the lifecycle of an Angular application, each time it’s created the ngOnInit
would be called creating a brand new subscription. This could quickly get out of hand, with our value being updated exponentially. This is creating what is called a “memory leak”. Memory leaks can wreak havoc on the performance of an application and in addition add unpredictable or unintended behaviors. Let’s read on to learn how to plug this leak.
Fixing the Leak on MyValueComponent
To fix the memory leak we need to augment the component class with an implementation of OnDestroy
and unsubscribe
from the subscription. Let’s update our component adding the following:
- Add
OnDestroy
to the typescriptimport
- Add
OnDestroy
to theimplements
list - Create a class field named
myValueSub: Subscription
to track our subscription - Set
this.myValueSub
equal to the value ofthis.myService.getValue().subscription
- Create a new class method named
ngOnDestroy
- Call
this.myValueSub.unsubscribe()
withinngOnDestroy
if a subscription has been set.
The updated component will look something like this:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { MyService } from './my.service';
@Component({
selector: 'app-my-value',
templateUrl: './my-value.component.html',
styleUrls: [ './my-value.component.css' ]
})
export class MyValueComponent implements OnInit, OnDestroy {
myValue: string;
myValueSub: Subscription;
constructor(private myService: MyService) {}
ngOnInit() {
this.myValueSub = this.myService.getValue().subscribe(value => this.myValue = value);
}
ngOnDestroy() {
if (this.myValueSub) {
this.myValueSub.unsubscribe();
}
}
}
Moving Beyond Memory Leaks
Great! Now you have some background on ngOnDestroy
and how cleaning up memory leaks is the primary use case for this lifecycle method. But what if you want to take it a step further and add additional cleanup logic? How about making server-side cleanup calls? Maybe preventing user navigation away?
As you read on we will discuss three methods to upgrade your ngOnDestroy
for optimum use.
Hot Tip #1 - Making NgOnDestroy Async
As with other lifecycle methods in Angular, you can modify ngOnDestroy
with async
. This will allow you to make calls to methods returning a Promise
. This can be a powerful way to manage cleanup activities in your application. As you read on we will explore an example of this.
Adding logic to call AuthService.logout from ngOnDestroy
Let’s pretend that you need to perform a server-side logout when MyValueComponent
is destroyed. To do so we would update the method as follows:
- Add
AuthService
to yourimports
- Add
AuthService
to yourconstructor
- Add
async
in front of the method namengOnDestroy
- Make a call to an
AuthService
tologout
using theawait
keyword.
Your updated MyValueComponent
will look something like this:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { MyService } from './my.service';
import { AuthService } from './auth.service';
@Component({
selector: 'app-my-value',
templateUrl: './my-value.component.html',
styleUrls: [ './my-value.component.css' ]
})
export class MyValueComponent implements OnInit, OnDestroy {
myValue: string;
myValueSub: Subscription;
constructor(private myService: MyService, private authService: AuthService) {}
ngOnInit() {
this.myValueSub = this.myService.getValue().subscribe(value => this.myValue = value);
}
async ngOnDestroy() {
if (this.myValueSub) {
this.myValueSub.unsubscribe();
}
await this.authService.logout();
}
}
Tada! Now when the component is destroyed an async
call will be made to log the user out and destroy their session on the server.
Hot Tip #2 - Ensure Execution During Browser Events
Many developers are surprised to learn that ngOnDestroy
is only fired when the class which it has been implemented on is destroyed within the context of a running browser session.
In other words, ngOnDestroy
is not reliably called in the following scenarios:
- Page Refresh
- Tab Close
- Browser Close
- Navigation Away From Page
This could be a deal-breaker when thinking about the prior example of logging the user out on destroy. Why? Well, most users would simply close the browser session or navigate to another site. So how do we make sure to capture or hook into that activity if ngOnDestroy
doesn’t work in those scenarios?
Decorating ngOnDestroy with HostListener
TypeScript decorators are used throughout Angular applications. More information can be found here in the official TypeScript docs.
To ensure that our ngOnDestroy
is executed in the above mentioned browser events, we can add one simple line of code to the top of ngOnDestroy
. Let’s continue with our previous example of MyValueComponent
and decorate ngOnDestroy
:
- Add
HostListener
to theimports
- Place
@HostListener('window:beforeunload')
on top ofngOnDestroy
Our updated MyValueComponent
will look something like this:
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { MyService } from './my.service';
import { AuthService } from './auth.service';
@Component({
selector: 'app-my-value',
templateUrl: './my-value.component.html',
styleUrls: [ './my-value.component.css' ]
})
export class MyValueComponent implements OnInit, OnDestroy {
myValue: string;
myValueSub: Subscription;
constructor(private myService: MyService, private authService: AuthService) {}
ngOnInit() {
this.myValueSub = this.myService.getValue().subscribe(value => this.myValue = value);
}
@HostListener('window:beforeunload')
async ngOnDestroy() {
if (this.myValueSub) {
this.myValueSub.unsubscribe();
}
await this.authService.logout();
}
}
Now our ngOnDestroy
method is called both when the component is destroyed by Angular AND when the browser event window:beforeunload
is fired. This is a powerful combination!
More about HostListener
@HostListener()
is an Angular decorator that can be placed on top of any class method. This decorator takes two arguments: eventName
and optionally args
. In the above example, we are passing window:beforeunload
as the DOM event. This means that Angular will automatically call our method when the DOM event window:beforeunload
is fired. For more information on @HostListener
check out the official docs.
If you want to use this to prevent navigation away from a page or component then:
- Add
$event
to the@HostListener
arguments - Call
event.preventDefault()
- Set
event.returnValue
to a string value of the message you would like the browser to display
An example would look something like this:
@HostListener('window:beforeunload', ['$event'])
async ngOnDestroy($event) {
if (this.myValueSub) {
this.myValueSub.unsubscribe();
}
await this.authService.logout();
$event.preventDefault();
$event.returnValue = 'A message.';
}
PLEASE NOTE: This is not officially supported by Angular!
OnDestroy
andngOnDestroy
suggest that there is no input argument onngOnDestroy
allowed. While unsupported, it does in fact still function as normal.
More about window:beforeunload
window:beforeunload
is an event fired right before the window
is unloaded. More details can be found in the documentation here: https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event.
A couple points to be aware of:
-
This event is currently supported in all major browsers EXCEPT iOS Safari.
-
If you need this functionality in iOS Safari then consider reviewing this Stack Overflow thread.
-
If you are using this event in an attempt to block navigation away you must set the
event.returnValue
to a string of the message you would like to display. More details in this example.
Conclusion
I realize that some of the tips recommended in this article are not mainstream and may generate some concern. Please remember as always to try these out and see if they fit for what you are doing in your application. If they work great! If not, then it’s ok to move on.
If you have any comments or questions feel free to contact me on Twitter
Additional Resources
I would highly recommend enrolling in the Ultimate Angular courses. It is well worth the money and I have used it as a training tool for new and experienced Angular developers. Follow the link below to signup.
Ultimate Courses: Expert online courses in JavaScript, Angular, NGRX and TypeScript