Angular course ยท Module 2: Components

Lifecycle hooks - the warrior's rituals

8 min read ยท
In this lesson6

Imagine a stopwatch component that counts the seconds of a training session. When should it start counting? When should it fetch from the server the data of the warrior it is supposed to show? And what happens when the learner goes to another view and the stopwatch disappears from the screen? If nobody stops it, it keeps ticking in the browser's memory like the ghost of a warrior swinging a sword in an empty dojo. This kind of bug is called a memory leak.

Every Angular component goes through the same lifecycle: Angular creates it, passes it data, displays its view, refreshes it after changes and finally removes it. At important moments of this cycle, Angular calls methods with fixed names, called lifecycle hooks. They are like the rituals of a samurai: one on entering the dojo, another before a fight, yet another on leaving.

Constructor or ngOnInit

The first moment in a component's life is the constructor. Angular calls it when it creates the instance of the class, and it is the time to prepare the equipment, that is, to inject dependencies such as the services from the fifth module. At this stage the component does not know the data from its parent yet. A field marked with the @Input() decorator, whose value the parent provides in its template (we will cover it in detail in the next lesson), has only its default value then.

The data from the parent is ready only in ngOnInit. Angular calls this hook once, after setting the first input values, which makes it the right place for startup logic and for fetching data from an API. This example shows the difference:

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-samurai',
  standalone: true,
  template: `<p>{{ greeting }}</p>`
})
export class SamuraiComponent implements OnInit {
  @Input() name = '';
  greeting = '';

  constructor() {
    console.log('Constructor, name:', this.name);
  }

  ngOnInit(): void {
    this.greeting = `Welcome, ${this.name}!`;
  }
}

When the parent writes <app-samurai name="Musashi">, the constructor logs an empty name to the console, because the parent has not passed it yet. By the time Angular calls ngOnInit, the name field already holds "Musashi", so the greeting comes out right. If you built it in the constructor, the learner would see "Welcome, !". My advice: leave the constructor for dependencies, and move everything that uses data from the parent or from the server to ngOnInit.

The OnInit and OnDestroy interfaces

In the example the class has the addition implements OnInit, and OnInit appears in the import from @angular/core. The OnInit interface is a contract: a class that implements it promises to have an ngOnInit() method. You write the word implements in the class declaration, after its name, for example export class DojoComponent implements OnInit {. TypeScript checks the promise, as a typo in the method name shows:

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-dojo',
  standalone: true,
  template: `<h1>{{ message }}</h1>`
})
export class DojoComponent implements OnInit {
  message = '';

  // typo: ngOnInt instead of ngOnInit
  ngOnInt(): void {
    this.message = 'The dojo is open!';
  }
}

The compiler rejects this code with a message saying that the DojoComponent class incorrectly implements the OnInit interface, because the ngOnInit method is missing. Without the implements addition the code would compile without an error, and Angular would silently never call the misnamed method, because it calls hooks by name. The message "The dojo is open!" would never appear, and you would hunt for the cause for hours. That is why you should always add implements with the right interface: it costs one word and catches typos before they reach a learner.

Every hook has its own interface, named without the ng prefix: OnInit for ngOnInit, OnDestroy for ngOnDestroy, OnChanges for ngOnChanges. You import all of them from @angular/core, and a class can implement several interfaces at once, separated by commas: implements OnInit, OnDestroy.

ngOnChanges - when the parent changes the order

The ngOnChanges hook reacts to changes of inputs. Angular calls it before ngOnInit if the component has inputs bound in the parent's template, and then on every change of their values. The hook receives an object of type SimpleChanges, in which a description of the change waits under the name of each changed input:

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-health-bar',
  standalone: true,
  template: `<p>Health: {{ health }}</p>`
})
export class HealthBarComponent implements OnChanges {
  @Input() health = 100;

  ngOnChanges(changes: SimpleChanges): void {
    const change = changes['health'];
    if (change && !change.firstChange) {
      console.log(`Health: ${change.previousValue} -> ${change.currentValue}`);
    }
  }
}

The description of a change has three fields: previousValue is the old value, currentValue is the new one, and firstChange is true on the first call, when the parent is just setting the input. That is why the condition !change.firstChange skips the start and logs only real changes, for example "Health: 100 -> 70" after a hit. Keep in mind that ngOnChanges reacts only to new values from the parent. If the component itself assigns this.health = 50, the hook is not called.

ngAfterViewInit - the view is ready for battle

After ngOnInit, Angular displays the component's template and its child components. When the whole view is ready, it calls ngAfterViewInit. This is the first moment when you can safely reach elements of the template, for example with the @ViewChild decorator, which points to an element marked in the template with a hash name such as #nameField. With the default settings, such an element does not exist yet in ngOnInit. Typical uses are putting the cursor into a form field or starting a library that draws a chart in a ready element. In the sixth module you will learn the newer, signal-based version of this decorator, the viewChild() function.

ngOnDestroy - cleaning up before leaving

The last ritual is ngOnDestroy. Angular calls it right before removing the component, for example when the learner goes to another view or when an @if block removes the component from the page. This is the place for cleaning up: stopping timers, cancelling subscriptions and detaching listeners that the component started itself. Here is a stopwatch that leaves no ghost behind:

import { Component, OnInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: `<p>Time: {{ seconds }} s</p>`
})
export class TimerComponent implements OnInit, OnDestroy {
  seconds = 0;
  private intervalId?: number;

  ngOnInit(): void {
    this.intervalId = window.setInterval(() => {
      this.seconds++;
    }, 1000);
  }

  ngOnDestroy(): void {
    window.clearInterval(this.intervalId);
  }
}

In ngOnInit the stopwatch starts setInterval, which increases seconds every second, and remembers its identifier in the intervalId field. In ngOnDestroy it passes that identifier to clearInterval, so the interval stops together with the component. Without this one line, every visit to the view would start another invisible stopwatch, and the application would slow down with every minute. Subscriptions to RxJS streams, which you will meet in the seventh module, are cleaned up in the same place.

Modern Angular also offers a shorter path. The inject() function gets a dependency right in a class field, and the DestroyRef object lets you write the cleanup right next to the code that starts something:

import { Component, DestroyRef, OnInit, inject } from '@angular/core';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: `<p>Time: {{ seconds }} s</p>`
})
export class TimerComponent implements OnInit {
  seconds = 0;
  private destroyRef = inject(DestroyRef);

  ngOnInit(): void {
    const id = window.setInterval(() => this.seconds++, 1000);
    this.destroyRef.onDestroy(() => window.clearInterval(id));
  }
}

The effect is the same: the interval stops the moment the component is removed. The difference is that the start and the cleanup sit side by side, so it is harder to forget the second one. Both paths are correct, and in this module's tasks you practice the classic ngOnDestroy.

Order of calls

Angular always calls the hooks in the same order. Here is the full list, from birth to departure:

  1. constructor - creating the instance of the class (once)
  2. ngOnChanges - before ngOnInit if there are inputs bound in the template, and then after each of their changes (many times)
  3. ngOnInit - initializing the component (once)
  4. ngDoCheck - custom change checking (many times)
  5. ngAfterContentInit - after inserting the content passed in through <ng-content> (once)
  6. ngAfterContentChecked - after checking that content (many times)
  7. ngAfterViewInit - after displaying the view and the children (once)
  8. ngAfterViewChecked - after checking the view (many times)
  9. ngOnDestroy - right before removing the component (once)

You do not have to implement all of them. In practice you will most often reach for four: ngOnChanges when you react to new data from the parent, ngOnInit for the start, ngAfterViewInit when you need a ready view, and ngOnDestroy for cleaning up. The constructor always comes first, and ngOnDestroy always comes last. Angular calls the hooks ending in Checked and ngDoCheck on every change check, which means very often, so do not put heavy calculations in them. You will develop fetching data in ngOnInit in the ninth module, when you communicate with an API.

Remember: the constructor prepares the equipment, ngOnInit starts the fight, and ngOnDestroy clears the battlefield before the warrior leaves.

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Which lifecycle hook is ideal for fetching data from an API when the component initializes?

  2. 2. Which lifecycle hook is used to clean up resources (unsubscribing, clearing timers) before the component is destroyed?

  3. 3. Which lifecycle hook is called on every change of an @Input value?

Hands-on tasks in the game

  • Vertical ordering

    Arrange the lifecycle hooks in the order Angular calls them (from first to last):

  • Code editor

    Implement the OnInit and OnDestroy interfaces, along with the corresponding lifecycle hook methods.

  • Horizontal ordering

    Arrange the single-line tokens of a class declaration implementing the OnInit hook:

Useful articles