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
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!