Lifecycle Hooks in Angular Redefined

Introduction Angular developers have long relied on lifecycle hooks to manage component behavior and interactions with the DOM. However, the introduction of signal-based APIs represents a significant shift in how developers can write reactive and declarative code. This article explores how to transition from traditional lifecycle hooks to modern signal-based APIs, demonstrating how these new tools simplify code, enhance reactivity, and reduce boilerplate. ngOnInit Why we used ngOnInit? The ngOnInit lifecycle hook was essential in traditional Angular development for basically 2 things: Working with @Input Properties: Ensuring @Input values are initialized and available for use after the component is created. Performing Network Requests: Fetching data once all inputs are available to ensure the component has the necessary information to render. Old Way: @Component({ ... }) class UserComponent implements OnInit { @Input() name: string; @Input() lastName: string; @Input() userId: string; fullName: string; userData: string; constructor(private httpClient: HttpClient) {} ngOnInit(): void { // getting access to inputs here this.fullName = `${this.name} ${this.lastName}`; this.httpClient .get(`https://api.example.com/user/${this.userId}`) .subscribe((data: any) => { this.userData = data.name; }); } } New way: @Component(...) class UserComponent { // signal based inputs!!! name = input.required(); lastName = input.required(); userId = input.required(); // no need for ngOnInit anymore fullName = computed(() => `${this.name()} ${this.lastName()}`); // signal based way yo do network request userData = resource({ request: () => ({ id: this.userId() }), loader: (request) => this.httpClient .get(`https://api.example.com/user/${request.id}`) }); } Key Benefits of Signals: Immediate Access to Inputs: No need to wait for lifecycle hooks; inputs are available as signals from the start. Reactive State Management: computed and resource ensure the UI is always in sync with the latest state. Simpler Code: Removes the boilerplate associated with lifecycle hooks and manual subscriptions. ngOnChanges Why we used ngOnChanges? The ngOnChanges lifecycle hook allowed developers to listen for changes in @Input properties and respond to them appropriately. Old way: @Component(...) class UserComponent implements OnChanges { @Input() name: string; @Input() lastName: string; fullName: string; ngOnChanges(changes: SimpleChanges): void { if (changes.name || changes.lastName) { this.fullName = `${this.name} ${this.lastName}`; console.log('Input changes detected:', this.fullName); } } } New way: @Component(...) class UserComponent { name = input.required(); lastName = input.required(); fullName = computed(() => `${this.name()} ${this.lastName()}`); } Pretty easy right? For me, subjectively and probably objectively this is much more understandable. ngOnDestroy Why we used ngOnDestroy: The ngOnDestroy lifecycle hook was traditionally used for cleanup tasks, such as unsubscribing from observables, clearing intervals, or detaching event listeners. Old way: @Component(...) export class CleanupComponent implements OnDestroy { private intervalId: any; constructor() { this.intervalId = setInterval(() => { console.log('Interval running'); }, 1000); } ngOnDestroy(): void { clearInterval(this.intervalId); console.log('Interval cleared'); } } New way @Component(...) export class CleanupComponent { constructor() { const intervalId = setInterval(() => { console.log('Interval running'); }, 1000); inject(DestroyRef).onDestroy(() => { clearInterval(intervalId); }); } } The "After" Hooks Why we used After Hooks: Angular's "After" hooks were used to interact with the DOM or its projected content once Angular completed rendering. These hooks include: ngAfterContentInit: Triggered once after Angular projects external content into the component's view. ngAfterContentChecked: Invoked after every check of the projected content. ngAfterViewInit: Runs once after Angular initializes the component's view and its children. ngAfterViewChecked: Called after every check of the component's view and its children. Old Way @Component({ template: '' }) export class FooComponent implements AfterViewInit, AfterViewChecked { @ViewChild('myCanvas') myCanvas: ElementRef; ngAfterViewInit() { initCharts(this.myCanvas.nativeElement); console.log('Charts initialized'); } ngAfterViewChecked() { console.log('View checked'); } } New way @Component({ template: '' }) export class FooComponent { myCanvas = viewChild('myCanvas'); constructor() { // runs once after the app rendered afterNextRender(() => { initCh

Jan 18, 2025 - 21:39
Lifecycle Hooks in Angular Redefined

Introduction

Angular developers have long relied on lifecycle hooks to manage component behavior and interactions with the DOM. However, the introduction of signal-based APIs represents a significant shift in how developers can write reactive and declarative code. This article explores how to transition from traditional lifecycle hooks to modern signal-based APIs, demonstrating how these new tools simplify code, enhance reactivity, and reduce boilerplate.

ngOnInit

Why we used ngOnInit?

The ngOnInit lifecycle hook was essential in traditional Angular development for basically 2 things:

  • Working with @Input Properties: Ensuring @Input values are initialized and available for use after the component is created.
  • Performing Network Requests: Fetching data once all inputs are available to ensure the component has the necessary information to render.

Old Way:

@Component({ ... })
class UserComponent implements OnInit {
  @Input() name: string;
  @Input() lastName: string;
  @Input() userId: string;

  fullName: string;
  userData: string;

  constructor(private httpClient: HttpClient) {}

  ngOnInit(): void {
    // getting access to inputs here
    this.fullName = `${this.name} ${this.lastName}`;
    this.httpClient
      .get(`https://api.example.com/user/${this.userId}`)
      .subscribe((data: any) => {
        this.userData = data.name;
      });
  }
}

New way:

@Component(...)
class UserComponent {
  // signal based inputs!!!
  name = input.required();
  lastName = input.required();
  userId = input.required();

  // no need for ngOnInit anymore
  fullName = computed(() => `${this.name()} ${this.lastName()}`);

  // signal based way yo do network request
  userData = resource({
    request: () => ({ id: this.userId() }),
    loader: (request) => 
      this.httpClient
        .get(`https://api.example.com/user/${request.id}`)
  });
}

Key Benefits of Signals:

  • Immediate Access to Inputs: No need to wait for lifecycle hooks; inputs are available as signals from the start.
  • Reactive State Management: computed and resource ensure the UI is always in sync with the latest state.
  • Simpler Code: Removes the boilerplate associated with lifecycle hooks and manual subscriptions.

ngOnChanges

Why we used ngOnChanges?
The ngOnChanges lifecycle hook allowed developers to listen for changes in @Input properties and respond to them appropriately.

Old way:

@Component(...)
class UserComponent implements OnChanges {
  @Input() name: string;
  @Input() lastName: string;

  fullName: string;

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.name || changes.lastName) {
      this.fullName = `${this.name} ${this.lastName}`;
      console.log('Input changes detected:', this.fullName);
    }
  }
}

New way:

@Component(...)
class UserComponent {
  name = input.required();
  lastName = input.required();

  fullName = computed(() => `${this.name()} ${this.lastName()}`);
}

Pretty easy right? For me, subjectively and probably objectively this is much more understandable.

ngOnDestroy

Why we used ngOnDestroy:

The ngOnDestroy lifecycle hook was traditionally used for cleanup tasks, such as unsubscribing from observables, clearing intervals, or detaching event listeners.

Old way:

@Component(...)
export class CleanupComponent implements OnDestroy {
  private intervalId: any;

  constructor() {
    this.intervalId = setInterval(() => {
      console.log('Interval running');
    }, 1000);
  }

  ngOnDestroy(): void {
    clearInterval(this.intervalId);
    console.log('Interval cleared');
  }
}

New way

@Component(...)
export class CleanupComponent {
  constructor() {
    const intervalId = setInterval(() => {
      console.log('Interval running');
    }, 1000);

    inject(DestroyRef).onDestroy(() => {
      clearInterval(intervalId);
    });
  }
}

The "After" Hooks

Why we used After Hooks:

  • Angular's "After" hooks were used to interact with the DOM or its projected content once Angular completed rendering. These hooks include:
  • ngAfterContentInit: Triggered once after Angular projects external content into the component's view.
  • ngAfterContentChecked: Invoked after every check of the projected content.
  • ngAfterViewInit: Runs once after Angular initializes the component's view and its children.
  • ngAfterViewChecked: Called after every check of the component's view and its children.

Old Way

@Component({ template: '' })
export class FooComponent implements 
  AfterViewInit, AfterViewChecked 
{
  @ViewChild('myCanvas') myCanvas: ElementRef;

  ngAfterViewInit() {
    initCharts(this.myCanvas.nativeElement);
    console.log('Charts initialized');
  }

  ngAfterViewChecked() {
    console.log('View checked');
  }
}

New way

@Component({ template: '' })
export class FooComponent {
  myCanvas = viewChild('myCanvas');

  constructor() {
    // runs once after the app rendered 
    afterNextRender(() => {
      initCharts(this.myCanvas());
      console.log('Charts initialized');
    });

    afterRender(() => {
      // runs every time app renders something 
      console.log('View updated');
    });
  }
}

ngDoCheck

Personally I used it very-very rarely in my practise.

Just use effect(), should be completely enough.

Summary

By transitioning to signal-based APIs, Angular developers can:

  • Eliminate the need for lifecycle hooks like ngOnInit, ngOnChanges, ngOnDestroy, and others.
  • Write more declarative, reactive, and maintainable code.
  • Simplify resource management, dependency tracking, and UI updates.
  • This evolution represents a significant step forward in Angular development, making applications cleaner, more performant, and easier to debug.

Wish you happy coding with this knowledge and hope you learn something new today!