Angular Lifecycle Hooks Best Practices

    Monday, April 8, 202410 min read484 views
    Angular Lifecycle Hooks Best Practices

    You might have worked with thousands of components during development as an Angular developer. However, do you know who manages all these processes from routing to your desired component to rendering the DOM?

    To load all the required data and binding properties to the component element tags finally destroying the component when you navigate to a different page. Angular lifecycle hooks that work for us continuously to maintain lightweight and high-performance single-page applications (in case you are not familiar, you can always find details here).

    A component's lifecycle is a series of events through which the parent component increments from its initialization to finally being destroyed. All the stages of this lifecycle are called hooks and are monitored precisely. Availing them to the developer to perform necessary logic at each stage of the lifecycle is handled by Angular lifecycle hooks. Directives have a similar lifecycle, as Angular creates, updates, and destroys instances in the execution.

    Angular's component lifecycle Phases:

    1. Creation: Angular creates the component or directive.

    2. Content Projection: If applicable, content projection occurs.

    3. Initialization: Component properties are initialized, and the component is prepared for rendering.

    4. Change Detection: Angular checks for changes in data bound properties and triggers change detection.

    5. Component Rendering: The component's view is rendered based on its current state.

    6. Component Destruction: Angular destroys the component or directive when it's no longer needed.

    Need of Lifecycle hooks in angular:

    Since we use the angular framework to develop single-page applications, it’s necessary to initialize the component when it needs to be loaded and destroy it when the user navigates to a different component/page. Lifecycle hooks are needed to tap into key events of the component during its lifecycle to initialize the data, view/check the child components rendered in DOM (document object model), respond to change detection, and cleanup logic before deletion of instance. 


    Here are a few things that wouldn’t have been possible without lifecycle hooks : 

    • Retrieving data from services and initializing variables on the loading of a component.

    • Detecting the changes in input property from the parent component and updating the bound properties accordingly.

    • Manipulating the content and applying styles to child components HTML after it's initialized properly.

    • avoid memory leaks by unsubscribing to data streams on destroying the component.

    How to Use Lifecycle Hooks in Your Angular Application?

    Using lifecycle hooks in angular components is seamless with the pre-defined lifecycle hook interfaces for each lifecycle hook method. You can start using any of the lifecycle hooks by implementing their interface and declaring the required lifecycle hook methods. lifecycle hooks can be used on components or directives as well.

    Simple Steps to Use Lifecycle Hooks

    1. Import the lifecycle hook interfaces from the angular core library

    2.  Implement the respective interface for the hook you want to use.

    3.  Declare the required method of implementing the interface defines.

      Example of component

    import { Component, Input, OnChanges,SimpleChanges } from '@angular/core';
    
    @Component({
      selector: 'app-example',
      template: '<p>{{ inputValue }}</p>',
    })
    
    export class ExampleComponent implements OnChanges {
     // Implemented OnChanges interface to use ngOnChanges lifecycle hook.
    
    @Input() inputValue: string;
    
     // Declared the ngOnChanges() method.
    
      ngOnChanges(changes: SimpleChanges): void {
        // write your code here
      }
    }

    if you've reached this point in the article, we assume you have understood the fundamentals of lifecycle hooks and their utilization. Now let's understand the decision-making process of selecting the appropriate lifecycle hook methods for specific scenarios, considering their use cases according to the component's stage following the best practices.

    Order of Execution of LifeCycle Hooks:

    LifeCycle Hooks

    Order of The angular lifecycles hooks

    1. ngOnChanges

    2. ngOnInit

    3. ngDoCheck

    4. ngAfterContentInit

    5. ngAfterContentInitChecked

    6. ngAfterViewInit

    7. ngAfterViewInitChecked

    8. ngOnDestroy

    Before diving deep into each lifecycle hook, it is important to know the concept of content projection in Angular. Will see its use cases in one of the lifecycle hooks (ngAfterContentInit).

    Content projection:

    It is a feature in Angular that allows you to insert content into a component's template from the outside. This external content can be specified in the component's template and projected into designated areas within the component's view. It's a way to create reusable components that can be customized with different content based on the context in which they are global or application services are used.

    1. parent.component.ts

    <app-my-component>
      <!-- Content to be projected into the component -->
      <p>This is projected content</p>
    </app-my-component>

    2. child.component.ts

    <div>
      <h2>Component Header</h2>
      <!-- This is where external content is projected -->
      <ng-content></ng-content>
    </div>

    Execution of LifeCycle Hooks

    1. ngOnChanges

    Angular calls ngOnChanges when component input properties change. It will load for the first time after the constructor and on the subsequent change detection. It provides information about the changes through the SimpleChanges object.

    NOTE :
    If your component has no input properties or you use it without providing any inputs, angular will not call ngOnChanges()

    Best Practices:

    1. Use this hook if your logic is dependent on the data bound properties change of the component.

    Ex: calculating age based on date of birth.

    import { Component, Input, OnChanges,SimpleChanges } from '@angular/core';
    
    @Component({
      selector: 'app-example',
      template: '<p>your age is : {{ age }}</p>',
    })
    
    export class ExampleComponent implements OnChanges {
    // input properties from parent component as string value
      @Input() dateOfBirth: string;  
    
    // local variable
      age : number;
    
      ngOnChanges(changes: SimpleChanges): void {
    	const currentYear = new Date(Date.now()).getYear();
    	const birthYear = new Date(this.dateOfBirth).getYear();
    	// age will be updated as soon as 'dateOfBirth' value changes
    	this.age = currentYear - birthYear
      }
    }

    NOTE:
    ngOnChanges triggers when we update the input properties of primitive data types but in case of Non-primitive data types it will not trigger properly. To handle this scenario you must update the non-primitive data types (objects) by reassigning them.

    Parent component

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-component',
      template: '<p></p>',
    })
    
    export class AppComponent {
      person = {
         name : 'Sanket',
         age  : 23
      }	
    
      updateDetails() {
          // updating the only object reference will not trigger ngOnChanges 
          this.person.name = 'Bhanu'
          // reassigning the object value will trigger the ngOnChanges
          this.person = {...this.person}
      }
     
    }
    1. Avoid performing expensive operations (like HTTP requests) directly in ngOnChanges. Consider using a dedicated service or an asynchronous approach.

    2. ngOnInit

    It's called once after ngOnChanges when angular initializes the component class. Use it for initialization logic, fetching initial data, or any setup that needs to happen once.

    Best Practices:

    1. Use this for retrieving the data from observables and assigning it to variables.

    import { Component, Input, OnInit} from '@angular/core';
    
    @Component({
      selector: 'app-example',
      template: '<p>your age is : {{ age }}</p>',
    })
    
    export class ExampleComponent implements OnInit { 
      constructor(private _formService : FormService, private dataService : DataService){}
    
    // local variables
      form : FormGroup;
      userDetails = {};	
    
      ngOnInit() {
          // Assign form from service 
          this.form = this._formService.userForm
    
         //  Subscribe to observable to fetch data and assign to variable to ensure    
             complete data on loading of component
          this._dataService.user$.subscribe(data => {
         		this.userDetails = data
            });
       }
    }
    1. Use the constructor for basic initialization, dependency injection, and setting up class properties. Avoid heavy logic or actions that depend on Angular-specific features.

    2. Use ngOnInit for Angular-specific initialization, such as working with input properties, setting up subscriptions, or fetching data from services. It ensures that the component class is fully initialized and Angular features are available.

    3. ngDoCheck

    This hook is triggered during every change detection cycle. It allows you to define custom change detection logic for your component class and allows you to check for changes outside the angular's default mechanism.

    NOTE:
    This happens frequently, so any operation you perform here impacts performance significantly.
    Use it for debugging purpose only.

    4. ngAfterContentInit

    Invoked after Angular projects external content into the component's template (content projection). Perform actions after projected content is initialized, such as accessing child components or querying content projection.

    NOTE:
    Called after very first ngDoCheck()

    Best practices:

    1. Use the AfterContentInit hook to perform logic in the child component based on content projected from a parent.

      For Ex: selecting the active panel from the accordion in the child component.

      child.component.ts

    import { Component, ContentChildren, QueryList, AfterContentInit } from '@angular/core';
    import { PanelComponent } from './panel.component';
    
    @Component({
      selector: 'app-accordion',
      template: '<ng-content></ng-content>',
    })
    export class AccordionComponent implements AfterContentInit {
      @ContentChildren(PanelComponent) panels: QueryList<PanelComponent>;
    
      ngAfterContentInit(): void {
        // Select the first panel by default
        const firstPanel = this.panels.first;
        if (firstPanel) {
          firstPanel.isActive = true;
        }
      }
    }

    parent.component.ts

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
        <div>
          <h1>Accordion Example</h1>
          <app-accordion>
            <app-panel title="Panel 1">
              <p>Content for Panel 1</p>
            </app-panel>
            <app-panel title="Panel 2">
              <p>Content for Panel 2</p>
            </app-panel>
            <app-panel title="Panel 3">
              <p>Content for Panel 3</p>
            </app-panel>
          </app-accordion>
        </div>
      `,
    })
    export class AppComponent {}

    5. ngAfterContentChecked

    Called after every check of the content projected into the component. Perform actions that need to be executed after each check of the projected content.

    NOTE:
    Called after ngAfterContentInit() and every ngDoCheck()

    Best practices:

    1. use the ngAfterContentChecked hook to check if the content is checked after initialization

    import { Component, AfterContentInit } from '@angular/core';
    
    @Component({
      selector: 'app-child',
      template: '<ng-content></ng-content>',
    })
    export class ChildComponent implements AfterContentInit {
      ngAfterContentInit(): void {
        console.log('Child component element content has been initialized.');
      }
    
      ngAfterContentChecked() : void {
        console.log('Child component element content has been checked.');
      }
    }
    

    6. ngAfterViewInit

    Invoked after the component's view (and child component views) has been initialized. Access and manipulate the component element after the view is created.

    NOTE:
    Called once after first ngAfterContentChecked()

    Best practices:

    1. append the new element in html after the child element's view has loaded properly

    import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: '<div #childElement></div>',
    })
    export class AppComponent implements AfterViewInit {
      // used to get a reference of child element
      @ViewChild('childElement', { static: true }) childElement: ElementRef;
    
      ngAfterViewInit(): void {
        // Access the native element using ViewChild
        const childElement = this.childElement.nativeElement;
    
        // Create a new nested element
        const nestedElement = document.createElement('div');
        nestedElement.className = 'nested-element';
        nestedElement.textContent = 'This is a nested element';
    
        // Append the nested element to the parent element
        childElement.appendChild(nestedElement);
      }
    }

    2. use this hook to ensure that the DOM view has loaded completely and related operations can be performed on it.

    NOTE:
    you can use the @ViewChild decorator to get a reference to a single element and @ViewChildren to get a reference to multiple elements from the DOM view.

    7. ngAfterViewChecked

    Called after every check of the component's view (and child component views). Perform actions that need to be executed after an angular check the component view.

    NOTE:
    Called after ngAfterViewInit() and every subsequent ngAfterContentChecked()

    Best practices:

    1. Use this to check the element after every change detection happens on DOM to get latest reference of the child element.

    import { Component, ViewChild, ElementRef, AfterViewInit, AfterViewChecked } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: '<div #childElement></div>',
    })
    export class AppComponent implements AfterViewInit, AfterViewChecked {
      // used to get a reference of child element
      @ViewChild('childElement', { static: true }) childElement: ElementRef;
    
      ngAfterViewInit(): void {
        // Access the native element using ViewChild
        const childElement = this.childElement.nativeElement;
    
        // Create a new nested element
        const nestedElement = document.createElement('div');
        nestedElement.className = 'nested-element';
        nestedElement.textContent = 'This is a nested element';
    
        // Append the nested element to the parent element
        childElement.appendChild(nestedElement);
      }
    
      ngAfterViewChecked(): void {
        // check the updated classes of child element after DOM refreshed	
        console.log(childElement.className);
      }
    }
    1. Avoid using this hook unnecessarily, it can impact the performance of the application due to the continuous change detection cycle.

    8. ngOnDestroy()

    Called just before angular destroys the component instance. Cleanup logic resources, unsubscribe from observables, detach event handlers, or perform any necessary cleanup.

    NOTE:
    This is the last hook method of component lifecycle. Entire lifecycle will be repeated after creating a new component instance.

    Best practices:

    1. Try to use async pipe to unsubscribe observables and data stream without the need to use ngOnDestroy(). It automatically subscribes to an Observable and handles the subscription and unsubscription for you.

      import { Component, OnInit } from '@angular/core';
      import { DataService } from './data.service';
      import { Observable } from 'rxjs';
      
      @Component({
        selector: 'app-root',
        template: `
          <h2>Items:</h2>
          <ul>
            <!-- Using the async pipe to subscribe to the observable -->
            <li *ngFor="let item of items$ | async">{{ item }}</li>
          </ul>
        `,
      })
      export class AppComponent implements OnInit {
        items$: Observable<string[]>;
      
        constructor(private dataService: DataService) {}
      
        ngOnInit(): void {
          // Assigning the Observable from the service to the items$ property
          this.items$ = this.dataService.getData();
        }
      }
    2. In case you need to subscribe to observable in .ts file then use ngOnDestroy() to prevent memory leaks by unsubscribing to observables efficiently.

      import { Component, OnInit, OnDestroy } from '@angular/core';
      import { DataService } from './data.service';
      import { Observable, Subscription } from 'rxjs';
      
      @Component({
        selector: 'app-root',
        template: `
          <h2>Items:</h2>
          <ul>
            <li *ngFor="let item of items$">{{ item }}</li>
          </ul>
        `,
      })
      export class AppComponent implements OnInit, OnDestroy {
        items$: Observable<string[]>;
        private dataSubscription: Subscription;
      
        constructor(private dataService: DataService) {}
      
        ngOnInit(): void {
          this.items$ = this.dataService.getData();
          // Subscribe to the Observable and store the subscription
          this.dataSubscription = this.items$.subscribe();
        }
      
        ngOnDestroy(): void {
          // Unsubscribe from the Observable to prevent memory leakage
          this.dataSubscription.unsubscribe();
        }
      }

    3. When working with the external library component in Angular, you can use the ngOnDestroy()lifecycle hook to perform cleanup logic, such as destroying the component instance to prevent memory leaks.

    Here's a simple example of using an ag-grid library:

    import { Component, OnInit, OnDestroy } from '@angular/core';
    import { GridOptions } from 'ag-grid-community';
    import 'ag-grid-enterprise'; // Import if you are using Ag-Grid Enterprise features
    
    @Component({
      selector: 'app-root',
      template: `
        <div class="ag-theme-alpine" style="height: 200px;">
          <ag-grid-angular
            #agGrid
            [gridOptions]="gridOptions"
            style="width: 100%; height: 100%;"
          ></ag-grid-angular>
        </div>
      `,
    })
    export class AppComponent implements OnInit, OnDestroy {
      gridOptions: GridOptions;
    
      ngOnInit(): void {
        // Initialize Ag-Grid configuration
        this.gridOptions = {
          // Ag-Grid options...
          columnDefs: [
            { headerName: 'Make', field: 'make' },
            { headerName: 'Model', field: 'model' },
            { headerName: 'Price', field: 'price' },
          ],
          rowData: [
            { make: 'Toyota', model: 'Celica', price: 35000 },
            { make: 'Ford', model: 'Mondeo', price: 32000 },
            { make: 'Porsche', model: 'Boxster', price: 72000 },
          ],
        };
      }
    
      ngOnDestroy(): void {
        // Destroy the Ag-Grid instance
        if (this.gridOptions.api) {
          this.gridOptions.api.destroy();
        }
      }
    }

    4. Cleaning up event handlers or timers is a crucial part of preventing memory leakages in Angular components. You can use ngOnDestroy() for that as well.

    import { Component, OnInit, OnDestroy } from '@angular/core';
    
    @Component({
      selector: 'app-timer-example',
      template: `
        <div>
          <p>Timer Value: {{ timerValue }}</p>
          <button (click)="startTimer()">Start Timer</button>
          <button (click)="stopTimer()">Stop Timer</button>
        </div>
      `,
    })
    export class TimerExampleComponent implements OnInit, OnDestroy {
      private timer: any;
      private timerValue: number = 0;
    
      ngOnInit(): void {
        // Example 1: Adding an event listener
        window.addEventListener('resize', this.handleResize);
    
        // Example 2: Starting a timer
        this.timer = setInterval(() => {
          this.timerValue++;
        }, 1000);
      }
    
      ngOnDestroy(): void {
        // Example 1: Removing the event listener to prevent
        window.removeEventListener('resize', this.handleResize);
    
        // Example 2: Clearing the timer 
        clearInterval(this.timer);
      }
    
      handleResize(): void {
        console.log('Window has been resized!');
      }
    
      startTimer(): void {
        // Example 2: Starting the timer again
        this.timer = setInterval(() => {
          this.timerValue++;
        }, 1000);
      }
    
      stopTimer(): void {
        // Example 2: Stopping the timer
        clearInterval(this.timer);
      }
    }

    Additional Tips:

    • Minimize Code in Hooks: Keep the logic within each lifecycle hook concise and focused on its intended purpose. This improves readability and maintainability.

    • Consider Alternatives: In some cases, using a service can be more efficient than performing actions directly in angular lifecycle hooks. Services can encapsulate reusable logic and handle resource management effectively.

    • Use with Caution: Angular lifecycle hooks are powerful, but use them judiciously. Overly complex logic can make components harder to test and maintain.

    • Testing: Test your components to ensure that the lifecycle hook is working as expected, especially their interactions with data changes and cleanup tasks.

    By following these best practices, you can leverage Angular's lifecycle hook effectively to create well-structured, maintainable, and efficient Angular applications.

    24

    Related articles

    This website uses cookies to analyze website traffic and optimize your website experience. By continuing, you agree to our use of cookies as described in our Privacy Policy.