ngOnInit() May Not Get Called Before ngOnDestroy() Is Called In Angular 4.4.6
Angular components have life-cycle hooks that you can tap into by implementing certain public methods on a given component. Two of the life-cycle hook methods - ngOnInit() and ngOnDestroy() - represent the birth and the death of the component, more or less. Until recently, I had always assumed that these two methods were symmetrical. Meaning, one always fired before the other. However, it turns out that the ngOnDestroy() method can be invoked before the ngOnInit() method ever has a chance to run. This is important to understand because it will necessarily influence the assumptions you can make in your ngOnDestroy() method implementations in Angular 4.4.6.
Run this demo in my JavaScript Demos project on GitHub.
Many of my ngOnInit() and ngOnDestroy() use-cases revolve around setting up subscriptions when a component is initialized; and then, unsubscribing from those subscriptions when the component is destroyed. However, I am not always so good about checking to see if the subscription object exists (in the ngOnDestroy() method) before calling ".unsubscribe()" on it. In a lot of cases, this coincidentally turns out to be OK. But, it's a dangerous assumption to make because the ngOnInit() method isn't always called before the ngOnDestroy() method is invoked.
To see this in action, we can setup a routable View component in Angular 4.4.6 that implements the OnInit and OnDestroy interfaces. Then, we can redirect away from the View component from within the View component's constructor and see which life-cycle hooks are called:
// Import the core angular services.
import { ActivatedRoute } from "@angular/router";
import { Component } from "@angular/core";
import { OnDestroy } from "@angular/core";
import { OnInit } from "@angular/core";
import { Router } from "@angular/router";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
styleUrls: [ "./child.component.css" ],
template:
`
<p>
This is the <strong>Child</strong> component.
</p>
<p>
<a [routerLink]="[ '../' ]">Back to Root</a>
</p>
`
})
export class ChildComponent implements OnInit, OnDestroy {
// I initialize the child-view component.
constructor( activatedRoute: ActivatedRoute, router: Router ) {
console.log( "Child :: Constructor" );
// If we have the OPTIONAL parameter to redirect, let's immediately redirect
// back to the root of the application.
// --
// CAUTION: This type of an action should probably be performed in the ngOnInit()
// method as it encompasses more than simple property initialization. However,
// I'm putting here in the constructor in order to demonstrate the functionality.
if ( activatedRoute.snapshot.paramMap.get( "redirect" ) ) {
console.warn( "Child :: Redirecting back to root." );
router.navigateByUrl( "/" );
}
}
// ---
// PUBLIC METHODS.
// ---
// I get called once when the component is about to be destroyed.
public ngOnDestroy() : void {
console.log( "Child :: ngOnDestroy" );
}
// I get called once after the component's inputs have been initialized.
public ngOnInit() : void {
console.log( "Child :: ngOnInit" );
}
}
As you can see, in the ChildComponent constructor, we have the option to immediately navigate away from thew View if the optional parameter, "redirect", is provided. This parameter will be embedded in one of the two links in the App component:
// Import the core angular services.
import { Component } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "my-app",
styleUrls: [ "./app.component.css" ],
template:
`
<p>
<a [routerLink]="[ 'child' ]">Child</a>
<br />
<a [routerLink]="[ 'child', { redirect: true } ]">Child w/ Redirect</a>
</p>
<router-outlet></router-outlet>
`
})
export class AppComponent {
// ...
}
With these two Angular components configured, if we run the app in the browser, navigate to the Child View using the "normal" link, and then manually navigate back to the root, we get the following output:
As you can see, the ngOnInit() method was invoked; then, when we navigated away from the Child view, the ngOnDestroy() method was invoked. Now, however, if we click the link with the optional "redirect" parameter, we see a different outcome:
As you can see, this time, the ChildComponent only logged the constructor and the ngOnDestroy() life-cycle method. There was no mention of the ngOnInit() life-cycle method. That's because the component didn't survive long enough for the ngOnInit() life-cycle hook to be used. In this case, I'm shooting myself in the foot a bit by redirecting in my "own" constructor; however, the same condition could also be met if one of the nested Views did the same thing. As such, this timing issue isn't confined to conditions presented within a single component.
The main take-away here is that you can't assume that the ngOnInit() life-cycle method is called before the ngOnDestroy() life-cycle method. That's not to say they are ever called out of order; only that the ngOnInit() life-cycle method may never have a chance to run at all. This means that in your ngOnDestroy() life-cycle method implementation, it is not safe to assume that properties initialized in the ngOnInit() life-cycle method even exist at the time the component is destroyed. You should code your ngOnDestroy() life-cycle methods defensively.
Want to use code from this post? Check out the license.
Reader Comments
@All,
Not directly related, but this demo has the potential to show-case another point-of-interest:
www.bennadel.com/blog/3361-view-components-may-get-unnecessarily-reinstantiated-under-certain-circumstances-in-angular-4-4-6.htm
If you go to the "Redirect" link in this demo. Then, hit the Browser Back button, you'll see that the Child Component gets created _twice_! This is the race-condition I'm seeing in the aforementioned link.
Hi,
It is incorrect use of constructor. You should use constructor only for injecting dependencies and you should move this logic to ngOnInit.
It is the same as pass the street on the red light. You may (use logic in constructor), but very often it can cause accident (the absence of ngOnInit).
Good Luck.
@Vitaliy,
You are correct in that it is considered a best practice to move subscription logic to the ngOnInit() life-cycle hook. However, that doesn't necessarily "solve" this problem. When the component tree is getting build, all of the descendant constructors get fired in series. What this means is that even if _your_ component is written "properly", there may be a nested component that is not. And, that nested component may not be one you have control over - it may be some 3rd-party vendor component. As such, this issue extends beyond the bounds of the current component. It's just a fact of life that you have to account for.... in my opinion.
@Vitaliy,
For me it happens even when redirect is inside ngOnInit of some parent component, which means not only when constructor is abused (Angular 5.0.3).
@Dmytro,
Ah, very interesting. I think that makes sense. Since the constructors() and the ngOnInit() methods run in top-down order, it would make sense that a higher-up ngOnInit() could signal to the Angular app to stop the top-down traversal.
That said, it is somewhat surprising behavior - that the router navigation is a "synchronous operation, so to speak. It's easy to assume that it would be "asynchronous", only performing the navigation after the component tree had been initialized. But, I think it is actually a really nice behavior - as it allows you to navigate away from a state early, short-circuiting lower-level logic in the component tree.