Angular Lifecycle Hooks: A Component's Life From ngOnInit to ngOnDestroy
Constructor vs ngOnInit, ngOnChanges with SimpleChanges, ngAfterViewInit where ViewChild is ready, and ngOnDestroy for cleaning up intervals. We log every hook to see the exact order.
Version note: The code and commands in this post are for Angular 12.
A Component's Life, From Birth to Goodbye
If you have tried to reach a child component or an input element with the @ViewChild we saw earlier, you may have run into undefined at some point. The code is right, the decorator is right, yet the value just isn't there. The reason is simple: you are running that line at the wrong moment.
Every component in Angular has a life. It gets created, receives its inputs, has its template rendered, gets updated as data changes, and one day is removed from the screen. At certain stops along that journey, Angular calls methods you have written. Those methods are called lifecycle hooks.
Think of moving into a new apartment. The moment you get the keys is the constructor; the place is yours, but it is empty. Once the furniture arrives and is in place, ngOnInit runs. Canceling the electricity and water when you move out is ngOnDestroy's job. Skip it and the bills keep coming.
Constructor or ngOnInit?
Let's start with the pair people mix up the most. Every component you generated with ng generate component came with both. Look at this component:
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `<p>{{ profileText }}</p>`
})
export class UserProfileComponent implements OnInit {
@Input() userId = 0;
profileText = '';
constructor() {
console.log('constructor, userId:', this.userId);
}
ngOnInit(): void {
console.log('ngOnInit, userId:', this.userId);
this.profileText = `Kullanıcı #${this.userId} yükleniyor...`;
}
}
In the parent template we use it as <app-user-profile [userId]="42"></app-user-profile>. In the console, the constructor line prints 0 and the ngOnInit line prints 42.
The constructor is the TypeScript class's own constructor, and Angular calls it while creating the component. At that point the inputs have not been assigned yet; Angular creates the object first and hands over the values afterwards. ngOnInit runs once, right after the inputs are set for the first time.
The practical rule: use the constructor to receive the dependencies you need, and do no other work there. How those dependencies arrive, in other words dependency injection, is something we will cover in detail in upcoming posts. Any setup that depends on inputs, such as loading data or computing values, belongs in ngOnInit.
The implements OnInit part is not required; Angular recognizes the method by its name. But if you write the interface, TypeScript warns you when you misspell the method name. Always write it.
ngOnChanges: Get Notified When an Input Changes
ngOnInit runs only once. So what if the input changes later? Picture a price tag that shows an up arrow when the price goes up and a down arrow when it drops:
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-price-tag',
template: `<p>{{ price }} TL <span *ngIf="trend">{{ trend }}</span></p>`
})
export class PriceTagComponent implements OnChanges {
@Input() price = 0;
trend = '';
ngOnChanges(changes: SimpleChanges): void {
const change = changes['price'];
if (change && !change.firstChange) {
this.trend = change.currentValue > change.previousValue ? '▲' : '▼';
}
}
}
ngOnChanges is called every time the value of an input coming from the parent changes. Its first call happens even before ngOnInit. The SimpleChanges object it receives keeps a record for each input that changed: previousValue is the old value, currentValue the new one, and firstChange tells you whether this is the initial assignment.
We skip the initial assignment with the firstChange check, because on the first arrival there is no old price to compare against. On every change after that, we compare old and new and pick the arrow.
Watch out for one trap. ngOnChanges catches reference changes. If you pass an array as an input and the parent adds an item with push, the array is still the same object, so ngOnChanges does not fire. Assign a new array and it does. We will look at the reason behind this in detail when we get to change detection.
ngDoCheck: The Guard Who Walks Every Round
After an event, a timer or some other trigger, Angular checks the component tree from top to bottom to update the screen. ngDoCheck runs on every one of those rounds, whether an input changed or not.
This is where you could catch the push problem above with a check of your own, for example by comparing the array's length with the previous round. But this hook can run dozens of times per second. Like a night guard, it tries every door on every round. Put heavy work in it and the whole app slows down. Know it exists, use it rarely.
Content and View Hooks: When Is ViewChild Ready?
Back to the undefined problem from the start. We want a search box to get focus as soon as the screen opens:
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
@Component({
selector: 'app-search-box',
template: `<input #searchInput placeholder="Ürün ara...">`
})
export class SearchBoxComponent implements OnInit, AfterViewInit {
@ViewChild('searchInput') searchInput!: ElementRef<HTMLInputElement>;
ngOnInit(): void {
console.log(this.searchInput); // undefined
}
ngAfterViewInit(): void {
this.searchInput.nativeElement.focus();
}
}
When ngOnInit runs, the template has not been rendered yet, so searchInput is empty. ngAfterViewInit runs once, after the component's own template and the child components inside it are fully created. It is the first place you can safely touch a ViewChild.
The exclamation mark at the end of the property comes from Angular 12's strict mode. TypeScript wants every property to have an initial value; with ! we are saying "Angular will fill this in, don't worry."
If the element is not inside an *ngIf, you can write @ViewChild('searchInput', { static: true }) and reach it in ngOnInit as well. In that case Angular resolves the query before the first render.
The view hooks have content siblings too. Content projected from outside with the ng-content we saw earlier is put in place before the component's own template. ngAfterContentInit runs at exactly that moment, and elements you grab with @ContentChild are ready there:
@ContentChild('cardTitle') cardTitle?: ElementRef;
ngAfterContentInit(): void {
console.log('Başlık geldi mi?', !!this.cardTitle);
}
This code lives in a card component used like <app-card><h3 #cardTitle>Sipariş Özeti</h3></app-card>. Once the projected heading is in place, it logs true.
ngAfterContentChecked and ngAfterViewChecked are the "every round" versions of the Init hooks. They run as often as ngDoCheck, and the same warning applies.
One last note: if you change a template-bound property inside ngAfterViewInit, you get an ExpressionChangedAfterItHasBeenCheckedError in development mode. Angular has already checked the screen and you changed the value right behind its back. DOM work like focusing is fine here; changing the data shown on screen is not.
ngOnDestroy: Turn Off the Lights on Your Way Out
A component is destroyed when it is removed with *ngIf or when you navigate to another page. Right before that, Angular calls ngOnDestroy. Look at this clock component:
import { Component, OnDestroy, OnInit } from '@angular/core';
@Component({
selector: 'app-clock',
template: `<p>{{ now.toLocaleTimeString() }}</p>`
})
export class ClockComponent implements OnInit, OnDestroy {
now = new Date();
private timerId = 0;
ngOnInit(): void {
this.timerId = window.setInterval(() => {
this.now = new Date();
console.log('tik');
}, 1000);
}
ngOnDestroy(): void {
window.clearInterval(this.timerId);
}
}
In ngOnInit we start an interval that runs every second and keep its id. In ngOnDestroy we use that id to stop the interval.
What if we had not written ngOnDestroy? The component leaves the screen, but the interval keeps living in the browser. The console keeps printing "tik". Every time you show and hide the component, another interval is added and they all run at once. That is a classic memory leak.
The same logic applies to event listeners you add to window and to observable subscriptions. We will cover the proper way to clean up subscriptions in detail when we get to RxJS in upcoming posts. The rule stays the same: whatever you open in ngOnInit, close in ngOnDestroy.
See the Order for Yourself
Enough theory. Let's log every hook in a single component:
import {
AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit,
Component, DoCheck, Input, OnChanges, OnDestroy, OnInit, SimpleChanges
} from '@angular/core';
@Component({
selector: 'app-lifecycle-demo',
template: `<p>Sayaç: {{ count }}</p>`
})
export class LifecycleDemoComponent implements OnChanges, OnInit, DoCheck,
AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
@Input() count = 0;
constructor() { this.log('constructor'); }
ngOnChanges(changes: SimpleChanges): void { this.log('ngOnChanges'); }
ngOnInit(): void { this.log('ngOnInit'); }
ngDoCheck(): void { this.log('ngDoCheck'); }
ngAfterContentInit(): void { this.log('ngAfterContentInit'); }
ngAfterContentChecked(): void { this.log('ngAfterContentChecked'); }
ngAfterViewInit(): void { this.log('ngAfterViewInit'); }
ngAfterViewChecked(): void { this.log('ngAfterViewChecked'); }
ngOnDestroy(): void { this.log('ngOnDestroy'); }
private log(hook: string): void {
console.log(`[demo] ${hook}`);
}
}
Add count = 0 and visible = true properties to AppComponent and write its template like this:
<button (click)="count = count + 1">Artır</button>
<button (click)="visible = !visible">Göster/Gizle</button>
<app-lifecycle-demo *ngIf="visible" [count]="count"></app-lifecycle-demo>
When the page loads, you see this order in the console:
[demo] constructor
[demo] ngOnChanges
[demo] ngOnInit
[demo] ngDoCheck
[demo] ngAfterContentInit
[demo] ngAfterContentChecked
[demo] ngAfterViewInit
[demo] ngAfterViewChecked
When you click the increment button, the Init hooks do not run again. Only the one that reports the change and the ones that run every round show up:
[demo] ngOnChanges
[demo] ngDoCheck
[demo] ngAfterContentChecked
[demo] ngAfterViewChecked
Click hide and a single line appears: [demo] ngOnDestroy. Show it again and all eight lines from the start come back, because this is a brand-new component object.
While the app starts up, you may see the Checked lines printed a few more times. Angular can run more than one check round at startup; as long as the order stays the same, there is nothing to worry about.
A Small Experiment
Add ClockComponent to your project and show and hide it from AppComponent with a button and *ngIf. First, comment out the clearInterval line in ngOnDestroy. Open and close the clock five or six times and watch the number of "tik" lines in the console multiply. Then bring the line back and see the difference.
As a second step, wire PriceTagComponent to a parent. Put a button in the parent that randomly raises or lowers the price and check that the arrow points the right way. Also see what happens on first load when you remove the firstChange check.
To show the time we called toLocaleTimeString(), and we typed "TL" next to the price by hand. Instead of writing helper methods in the class to format a date the way you want or show a number as currency, there is a way to do that transformation in a single line right in the template. Next up, we look at Angular's pipes.