Welcome to our new blog series, Exploring Angular Lifecycle Hooks! There’s going to be nothing quite like this available on the web, as we will be promoting best practices, revealing hidden tips and tricks, and getting a real grasp on how and when these hooks are called.
Before we dive into the first installment of the series, let’s review briefly all of the available lifecycle hooks and where they can be used.
Table of contents
Available Lifecycle Hooks covered in this series:
Lifecycle Hooks Can Be Used On:
- Components
- Directives
Here is a component with all eight (8) hooks implemented:
import {
AfterContentChecked,
AfterContentInit,
AfterViewChecked,
AfterViewInit,
Component,
DoCheck,
OnChanges,
OnDestroy,
OnInit
} from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: './home.component.html'
})
export class HomeComponent
implements
OnChanges,
OnInit,
DoCheck,
AfterContentInit,
AfterContentChecked,
AfterViewInit,
AfterViewChecked,
OnDestroy {
ngOnChanges() {}
ngOnInit() {}
ngDoCheck() {}
ngAfterContentInit() {}
ngAfterContentChecked() {}
ngAfterViewInit() {}
ngAfterViewChecked() {}
ngOnDestroy() {}
}
Let’s kick the series off with one of the most misunderstood hooks—ngOnDestroy—and answer those questions you’re dying to ask.
OnDestroy
’s primary purpose, according to the Angular Docs is to perform “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.”
If you’re like me, you had a few questions after reading the docs. Clean up what? Avoid memory leaks? Hey—that’s not very specific, it sounds like we need to uncover this a bit more. So here we go!
In this article, we will review how to implement OnDestroy
, common use cases for OnDestroy
, and wrap-up with a bonus enhancement to OnDestroy
that will allow it to be executed with browser events.
A Brief Overview
OnDestroy
is an Angular lifecycle method, that can hooked into on components
and directives
in Angular. By defining a specific method named ngOnDestroy
on our class, we are telling the Angular runtime, that it should call our method at the appropriate time. This is a powerful and declarative way to add specific cleanup logic to the end of our class lifecycle.
Implementing OnDestroy
As with other Angular lifecycle methods, adding the actual hook for OnDestroy
is relatively simple.
Add OnDestroy after the implements keyword
The first step to implementing OnDestroy
is to add OnDestroy
after the implements
keyword on a component
or directive
.
Here’s a typical component without any lifecycle hooks:
import { Component } from '@angular/core';
@Component({...})
export class MyValueComponent {}
Our first change is to import OnDestroy
from Angular’s core and then create a contract with implements OnDestroy
:
Fun Fact Time: Technically it’s not required to implement the interface, Angular will call
ngOnDestroy
regardless, however, it’s very helpful for type-checking, and to allow other developers to quickly identify which lifecycle hooks are in use on this class.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
import { Component, OnDestroy } from '@angular/core';
@Component({...})
export class MyValueComponent implements OnDestroy {}
Add the ngOnDestroy method to our class
Now that we have added the OnDestroy
after implements
the TypeScript intellisense will underline the class declaration in red, giving a warning that ngOnDestroy
was not found. Let’s fix that by creating our new ngOnDestroy
method.
Example Component Before:
import { Component, OnDestroy } from '@angular/core';
@Component({...})
export class MyValueComponent implements OnDestroy {}
Example Component After:
import { Component, OnDestroy } from '@angular/core';
@Component({...})
export class MyValueComponent implements OnDestroy {
ngOnDestroy() {
// cleanup logic goes here
}
}
You’ll also note that this lifecycle hook takes no arguments, unlike some of the others we’ll be covering in later articles.
Common Use Cases
As you can see, implementing OnDestroy
is fairly straightforward. Now, let’s explore some common use cases for OnDestroy
. At the beginning of the article, we mentioned that Angular recommends the following: “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.” Let’s explore this further.
Avoiding Memory Leaks with OnDestroy
We want to avoid memory leaks, but what are they? According to Google’s definition, a memory leak is “a failure in a program to release discarded memory, causing impaired performance or failure.” Memory leaks are typically created from not understanding how things work and wreak havoc on app performance. Let’s explore an example of one such memory leak - so you’re primed to tackle your OnDestroy logic in future!
A Leaky ShowUserComponent
Let’s imagine a scenario wherein we have a component that has one button. When we click the button a call is made to a method on a AuthService
that returns an Observable
containing the name of the logged in user. The button click event subscribes to this Observable
and displays a window alert with the username.
Here’s how the component might look before implementing OnDestroy
:
show-user.component.ts
import { Component } from '@angular/core';
import { AuthService } from './auth.service';
@Component({...})
export class ShowUserComponent {
constructor(private authService: AuthService) {}
showLoggedInUser() {
this.authService
.getLoggedInUserName()
.subscribe(username => window.alert(`You are logged in as ${username}!`));
}
}
show-user.component.html
<button (click)="showLoggedInUser()">Show Logged In User</button>
At first glance, you might say, “This component looks great, it subscribes to the service and shows an alert on click”. You’d be correct, but what do you think would happen if this ShowUserComponent
was used in the AppComponent
and displayed with an *ngIf
conditionally. Perhaps a scenario exists where the ShowUserComponent
is destroyed and then displayed again.
Well, I can tell you what would happen, some really odd, strange behavior. If the component was instantiated, the user clicked the button and alert displayed, then one subscription would be created. Then let’s say, the component was re-created and the user clicked the button again, how times would the alert display? Two times, at least! This is because a second subscription would be created and then fired when the button is clicked.
This is creating the “memory leak” and could quickly get out of hand, with our alert being shown exponentially (just imagine the impact across an entire codebase without cleaning things up properly!). Let’s read on to learn how to plug this memory leak using OnDestroy
.
Fixing the Leak on ShowUserComponent
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
myUserSub: Subscription
to track our subscription - Set
this.myUserSub
equal to the value ofthis.authService.getLoggedInUserName().subscription
- Create a new class method named
ngOnDestroy
- Call
this.myUserSub.unsubscribe()
withinngOnDestroy
if a subscription has been set.
Best Practice: Notice that we are checking if
this.myUserSub
is “truthy” before attempting to callunsubscribe
. This avoids a potential situation wherein the subscription may have never been created, thus preventing a ghastlyunsubscribe is not a function
error message.
The updated component will look something like this:
import { Component, OnDestroy } from '@angular/core';
import { AuthService } from './auth.service';
import { Subscription } from 'rxjs';
@Component({...})
export class ShowUserComponent implements OnDestroy {
myUserSub: Subscription;
constructor(private authService: AuthService) {}
showLoggedInUser() {
this.myUserSub = this.authService
.getLoggedInUserName()
.subscribe(username => window.alert(`You are logged in as ${username}!`));
}
ngOnDestroy() {
if (this.myUserSub) {
this.myUserSub.unsubscribe();
}
}
}
Now we can ensure that our alert will only ever be displayed once per button click.
Great! Now we have some background on ngOnDestroy
and how cleaning up memory leaks is the primary use case for this lifecycle method.
Additional Cleanup Logic
Exploring further, we find more examples of use cases for ngOnDestroy
including making server-side cleanup calls, and preventing user navigation away from our component. Let’s explore these additional scenarios, and how we can enhance ngOnDestroy
to meet our needs.
Making NgOnDestroy Async
As with other lifecycle methods in Angular, we can modify ngOnDestroy
with async
. This will allow us to make calls to methods returning a Promise
. This can be a powerful way to manage cleanup activities in our application. As we read on we will explore an example of this.
Adding logic to call AuthService.logout from ngOnDestroy
Let’s pretend that we need to perform a server-side user logout when ShowUserComponent
is destroyed. To do so we would update the method as follows:
- Add
async
in front of the method namengOnDestroy
- Make a call to an
AuthService
tologout
using theawait
keyword.
Our updated ShowUserComponent
will look something like this:
import { Component, OnDestroy } from '@angular/core';
import { AuthService } from './auth.service';
@Component({...})
export class ShowUserComponent implements OnDestroy {
myUserSub: Subscription;
constructor(private authService: AuthService) {}
showLoggedInUser() {
this.myUserSub = this.authService
.getLoggedInUserName()
.subscribe(username => window.alert(`You are logged in as ${username}!`));
}
async ngOnDestroy() {
if (this.myUserSub) {
this.myUserSub.unsubscribe();
}
await this.authService.logout();
}
}
Tada! Now when the component is destroyed an async
call will be made to logout the user and destroy their session on the server.
Unsubscribe versus takeUntil
As an alternative to manually calling unsubscribe
you could take things a step further and make use of the takeUntil
RxJS operator to “short-circuit” the subscription when a value is emitted.
Confused? Well imagine this…
- Add a new private property to your component named
destroyed$
. This property will be aReplaySubject<boolean> = new ReplaySubject(1)
, meaning it only ever emits one boolean value. - Add a
.pipe
to thethis.authService.getLoggedInUserName()
subscription - Pass
takeUntil(this.destroyed$)
into thepipe
method - Update the
ngOnDestroy
method to push a new value to thedestroyed$
subject, usingthis.destroyed$.next(true)
- Update the
ngOnDestroy
method to callcomplete
on thedestroyed$
subject.
The finished component will look something like this:
import { Component, OnDestroy } from '@angular/core';
import { AuthService } from './auth.service';
import { ReplaySubject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({...})
export class ShowUserComponent implements OnDestroy {
private destroyed$: ReplaySubject<boolean> = new ReplaySubject(1);
constructor(private authService: AuthService) {}
showLoggedInUser() {
this.myUserSub = this.authService
.getLoggedInUserName()
.pipe(takeUntil(this.destroyed$))
.subscribe(username => window.alert(`You are logged in as ${username}!`));
}
async ngOnDestroy() {
this.destroyed$.next(true);
this.destroyed$.complete();
await this.authService.logout();
}
}
With this new method in place, we no longer need to keep track of each subscription, check for truthy and call unsubscribe. The real power of this comes into play, when we have multiple subscriptions that need to be unsubscribed from. At that point, we would just add the takeUntil
to each subscription, and then leave our updated ngOnDestroy
to emit the destroyed$
true value to all the subscriptions.
- Inspiration taken from Stack Overflow
Advanced ngOnDestroy, Browser Events
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 ShowUserComponent
and decorate ngOnDestroy
:
- Add
HostListener
to theimports
- Place
@HostListener('window:beforeunload')
on top ofngOnDestroy
Our updated ShowUserComponent
will look something like this:
import { Component, OnDestroy, HostListener } from '@angular/core';
import { AuthService } from './auth.service';
@Component({...})
export class ShowUserComponent implements OnDestroy {
myUserSub: Subscription;
constructor(private authService: AuthService) {}
showLoggedInUser() {
this.myUserSub = this.authService
.getLoggedInUserName()
.subscribe(username => window.alert(`You are logged in as ${username}!`));
}
@HostListener('window:beforeunload')
async ngOnDestroy() {
if (this.myUserSub) {
this.myUserSub.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
For a deep dive on Angular decorators checkout out our in-depth writeup!
@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 we 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 we 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 = 'Are you sure you wanna close the page yo?.';
}
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 MDN docs.
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
That brings us to the end of the article, hopefully you have been able to glean some good advice on why and how to use OnDestroy
logic in your applications. I will leave you with a couple best practices that should be adopted:
- Always implement the
OnDestroy
interface - Always unsubscribe from subscriptions to prevent un-savory memory leaks
- Always check if a subscription has been created before attempting to unsubscribe from it.
To learn more techniques, best practices and real-world expert knowledge I’d highly recommend checking out my Angular courses - they will guide you through your journey to mastering Angular to the fullest!