Complete Guide: Angular lifecycle hooks

Angular
9 minutes read

Introduction

A component instance in Angular has a lifecycle that starts when Angular instantiates the component class and renders the component view along with its child views. The lifecycle continues with change detection, as Angular checks to see when data-bound properties change, and updates both the view and the component instance as needed. The lifecycle ends when Angular destroys the component instance and removes its rendered template from the DOM. 

Directives have a similar lifecycle, as Angular creates, updates, and destroys instances in the course of execution.

Angular applications can use lifecycle hook methods to tap into key events in the lifecycle of a component or directive to initialize new instances, initiate change detection when needed, respond to updates during change detection, and clean up before the deletion of instances.

Angular calls these hook methods in the following order:

  1. ngOnChanges: When an input/output binding value changes.
  2. ngOnInit: After the first `ngOnChanges`.
  3. ngDoCheck: Developer’s custom change detection.
  4. ngAfterContentInit: After component content is initialized.
  5. ngAfterContentChecked: After every check of component content.
  6. ngAfterViewInit: After a component’s views are initialized.
  7. ngAfterViewChecked: After every check of a component’s views.
  8. ngOnDestroy: Just before the component/directive is destroyed.

Project setup

We will create a new project for practical understanding using the Angular CLI:

ng new life-cycle-hooks

Then run the following commands to create the needed components

ng g c components/parent

ng g c components/child

Then, add the following starter code.

<!-- app.component.html -->

<app-parent></app-parent>
// components/parent/parent.component.ts

export class ParentComponent {
  userName = 'Maria';

  updateUser() {
     this.userName = 'Chris';
  }
}
<!-- components/parent/parent.component.html -->

<button (click)="updateUser()">Update</button>

<br/>
<br/>

<app-child [userName]="userName"></app-child>
// components/child/child.component.ts

export class ChildComponent {
  @Input() userName = '';

}
<!-- components/child/child.component.html -->

Here is the user name: {{ userName }}

With the above code, the UI looks like below:

ngOnChanges

This method is called once on a component’s creation and then every time changes are detected in one of the component’s input properties. It receives a SimpleChanges object as a parameter, which contains information regarding which of the input properties has changed – in case we have more than one – and its current and previous values.

Note that if your component has no inputs or you use it without providing any inputs, the framework will not call `ngOnChanges()`.

This is one of the lifecycle hooks which can come in handy in multiple use cases. It is very useful if you need to handle any specific logic in the component based on the received input property.

To demonstrate, let’s add the following to our code:

// components/child/child.component.ts

export class ChildComponent implements OnChanges {
  @Input() userName = '';

  ngOnChanges(changes:SimpleChanges) {
    console.log('ngOnChanges triggered', changes);
 }
}

Check the `console`, we should see that the `console.log()` is called after opening the application for the first time.

Notice that, the received `changes` object has three keys `currentValue`, `previousValue`, and `firstChange`, they work as they sound.

We could say that we want to change the `userName` value if it’s not the first change, or if the current value is only `Chris`. We could do anything here, let’s implement the second case.

// components/child/child.component.ts

export class ChildComponent implements OnChanges{

  @Input() userName = '';

  ngOnChanges(changes:SimpleChanges) {
     console.log('ngOnChanges triggered', changes);

     if (!changes['userName'].isFirstChange()){
        if (changes['userName'].currentValue === "Chris") {
           this.userName = 'Hello ' + this.userName
        } else {
           this.userName = changes['userName'].previousValue
        }
     }
  }
}

Here we update the `userName` value if it’s not the first value (because it won’t take the first input if we didn’t add this condition) and if the current value is `Chris` only.

Now, if we click on the `Update` button, we should see that the `console.log()` function is triggered one more time. This happens because the `@Input()` value changed (from `Maria` to `Chris`). If we keep clicking on the `Update` button we will not see anything new on the console, this happens because the `@Input()` is not changed.

Also, if we checked the console’s output we will see that the received object is changed like this

ngOnInit

This method is called only once during the component lifecycle after the first `ngOnChanges` call. `ngOnInit()` is still called even when `ngOnChanges()` is not, which is the case when there are no template-bound inputs.

This is one of the most used lifecycle hooks in Angular. Here is where you might set requests to the server to load content, maybe create a FormGroup for a form to be handled by that component, set subscriptions, and much more. It is where you can perform any initializations shortly after the component’s construction.

But what is the advantage of the `ngOnInit` hook if the same work (initializing a `FormGroup` or getting data from the server) could be done on the component’s `constructor()`? Well, let me explain.

Following are some of the main key points:

  • The `constructor()` 
    • The default method of the class is executed when the class is instantiated and ensures proper initialization of fields in the class and its subclasses. 
    • The dependency Injector (DI) in Angular, analyses the constructor parameters and when it creates a new instance by calling `new MyClass()` it tries to find providers that match the types of the constructor parameters, resolves them, and passes them to the constructor like `new MyClass(someArg)`
    • Should only be used to initialize class members but shouldn’t do actual work. This is because the `constructor` is called before `ngOnInit`, at this point the component hasn’t been created yet, only the component class has been instantiated thus the dependencies are brought in, but the initialization code will not run.
  • The `ngOnInit()`
    • Is a life cycle hook called by Angular to indicate that Angular is done creating the component.
    • Should be used for all the initialization/declaration. Because at this point the component will be initialized.

To demonstrate, let’s add the following to our code:

// components/parent/parent.component.ts

export class ParentComponent implements OnInit {
 ...

  ngOnInit() {
     console.log('ngOnInit from the parent component');
  }

  ...
}
// components/child/child.component.ts

export class ChildComponent implements OnInit, OnChanges {
  ...

  ngOnInit() {
     console.log('ngOnInit from the child component');
  }

  ...
}

We should see the following result on the console

It makes sense that the parent component is created first (`ngOnInit` from the parent component is triggered first), then its child component.

If we tried to click on the `Update` button, the `ngOnInit` will not trigger. Because, as I mentioned above, it only triggers once.

ngDoCheck

This hook can be interpreted as an “extension” of `ngOnChanges`. You can use this method to detect changes that Angular can’t or won’t detect. It is called in every change detection, immediately after the `ngOnChanges` and `ngOnInit` hooks.

This hook is costly since it is called with enormous frequency; after every change detection cycle no matter where the change occurred. Therefore, its usage should be careful to not affect the user experience.

To demonstrate, let’s add the following to our code:

// components/child/child.component.ts

export class ChildComponent implements OnInit, OnChanges, DoCheck {
  @Input() userName = '';

  constructor() {
  }

  ...

  ngDoCheck() {
     console.log('ngDoCheck triggered');
  }

...
}

We should see the following result after running the application.

If we click on the `Update` button we should see that the `ngOnChanges` and the `ngDoCheck` are triggered

And if we keep clicking on the `Update` we should see that only the `ngDoCheck` is triggered after each click, this happens because `ngDoCheck` captures a change. 

How does `ngDoCheck` capture a change if there is no change in the `@Input()` property? 

Well, since Angular tracks object reference and we mutate the object without changing the reference Angular won’t pick up the changes and it will not run change detection for the component. Thus the new name property value will not be re-rendered in DOM. Luckily, we can use the `ngDoCheck` lifecycle hook to check for object mutation and notify Angular.

ngAfterContentInit

This method is called only once during the component’s lifecycle, after the first `ngDoCheck`. Within this hook, we have access for the first time to the ElementRef of the ContentChild after the component’s creation; after Angular has already projected the external content into the component’s view.

To demonstrate, let’s add the following to our code:

<!-- components/parent/parent.component.html -->

...

<app-child [userName]="userName">
  <div #contentWrapper>foo</div> <!-- <== Add this -->
</app-child>
// components/child/child.component.ts

export class ChildComponent implements OnInit, OnChanges, DoCheck, AfterContentInit {
  ...

  @ViewChild('wrapper') wrapper!: ElementRef;
  @ContentChild('contentWrapper') content!: ElementRef;

 ...

  ngAfterContentInit() {
     console.log('ngAfterContentInit - wrapper', this.wrapper);
     console.log('ngAfterContentInit - 'contentWrapper', this.content);
  }

...
}
<!-- components/child/child.component.html -->

...

<div #wrapper>
  <ng-content></ng-content>
</div>

In the above code snippet, we projected content from the `Parent` component to the `Child` component. To learn more about content projection, check this doc.

At this point, we only have access to the projected content (`contentWrapper` has the value of the projected content). Moreover, the component’s template is not initialized yet (`wrapper` is `undefined`). It will be initialized and ready to be accessed on the `ngAfterViewInit` hook.

ngAfterContentChecked

This method is called once during the component’s lifecycle after `ngAfterContentInit` and then after every subsequent `ngDoCheck`. It is called after Angular has already checked the content projected into the component in the current digest loop.

To demonstrate, let’s add the following to our code:

// components/child/child.component.ts

export class ChildComponent implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked {
 

  ngAfterContentChecked(): void {
     console.log('ngAfterContentChecked triggered');
  }
}

If we click on the `Update` button, the `ngAfterContentChecked` will trigger each time, as well as `ngDoCheck`

ngAfterViewInit

This method is called only once during the component’s lifecycle, after `ngAfterContentChecked`. Within this hook, we have access for the first time to the ElementRef of the ViewChildren after the component’s creation; after Angular has already composed the component’s views and its child views.

This hook is useful when you need to load content on your view that depends on its view’s components; for instance when you need to set a video player or create a chart from a canvas element

To demonstrate, let’s add the following to our code:

// components/child/child.component.ts

export class ChildComponent implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit {

...

  ngAfterViewInit(): void {
     console.log('ngAfterViewInit - wrapper', this.wrapper);
  }

... 
}

At this point, the component’s template is created and we have access to it.

ngAfterViewChecked

This method is called once after `ngAfterViewInit` and then after every subsequent `ngAfterContentChecked`. It is called after Angular has already checked the component’s views and its child views in the current digest loop.

To demonstrate, let’s add the following to our code:

// components/child/child.component.ts

export class ChildComponent implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit,
     AfterViewChecked {

  @Input() userName = '';
  @ViewChild('wrapper') wrapper!: ElementRef;
  @ContentChild('contentWrapper') content!: ElementRef;

  constructor() {
  }

...

  ngAfterViewChecked(): void {
     console.log('ngAfterViewChecked triggered');
  }

...
}

If we continue clicking on the `Update` button many times, the `ngAfterViewChecked` will be triggered each time, as well as, `ngDoCheck` and `ngAfterContentChecked`.

ngOnDestroy

Lastly, this method is called only once during the component’s lifecycle, right before Angular destroys it. Here is where you should inform the rest of your application that the component is being destroyed, in case there are any actions to be done regarding that information.

Also, it is where you should put all your cleanup logic for that component. For instance, it is where you can remove any local storage information and most importantly unsubscribe observables/detach event handlers/stop timers, etc. to avoid memory leaks.

Note that the `ngOnDestroy` is not called when the user refreshes the page or closes the browser. So, in case you need to handle some cleanup logic on those occasions as well, you can use the HostListener decorator, as shown below:

@HostListener(‘window:beforeunload’)
ngOnDestroy() {
  // Insert Logic Here!
}

To demonstrate, let’s add the following to our code:

// components/parent/parent.component.ts

export class ParentComponent implements OnInit {
  ...

  isChildDestroyed = false;

 ...

  destroy() {
     this.isChildDestroyed = true;
  }

...
}
<!-- components/parent/parent.component.html -->

...

<button (click)="destroy()">Destroy Child</button>


<app-child *ngIf="!isChildDestroyed"
          [userName]="userName">
  <div #contentWrapper>foo</div>
</app-child>

<div *ngIf="isChildDestroyed">Child component is destroyed! :(</div>

...
// components/child/child.component.ts

export class ChildComponent implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit,
     AfterViewChecked, OnDestroy {

 ...

  ngOnDestroy(): void {
     console.log('Child component is destroyed! :(');
  }

...
}

This is our updated UI

If we click on the `Destroy Child` button, notice that the `ngOnDestroy` function will be triggered and the component will be removed from the DOM.

The below picture represents the DOM before clicking on the `Destroy Child` button.

And the following image represents the DOM after clicking on the `Destroy Child` button.

The Full Scene

Long story short, we can understand the lifecycle hooks by splitting the process into two steps,” first-time hooks”, and “in every change detection cycle hooks”.

Step 1: “First-time hooks”, the triggered hooks are:

  • onChanges
  • onInit
  • doCheck
  • afterContentInit
  • afterContentChecked
  • afterViewInit
  • afterViewChecked

Step 2: “In every change detection cycle hooks”, the triggered hooks are:

  • onChanges
  • doCheck
  • afterContentChecked
  • afterViewChecked

The following diagram shows exactly how the Angular component/directive lifecycle works

Further Reading

Conclusion 

We understood Angular Lifecycle Hooks, their objectives, and when they are called and they can be very useful when creating Angular applications. Therefore it is important to know how they work and what you can achieve with them to be able to apply them whenever you might need them. Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *