In Angular durchläuft jede Komponente einen festgelegten Lebenszyklus (Lifecycle) von der Initialisierung bis zur Zerstörung. Um an bestimmten Punkten dieses Lebenszyklus eingreifen zu können, bietet Angular sogenannte “Lifecycle Hooks”. Diese ermöglichen es Entwicklern, Code zu definierten Zeitpunkten im Leben einer Komponente auszuführen.
Diese Hooks sind wichtige Bestandteile des Angular-Frameworks und ermöglichen eine präzise Steuerung von Komponenten während ihres Lebenszyklus. Durch die Implementierung dieser Interfaces können Entwickler den vollen Nutzen aus dem Komponenten-Lebenszyklus ziehen.
Hier sind alle wichtigen Lifecycle Hooks in ihrer Ausführungsreihenfolge:
interface OnChanges {
ngOnChanges(changes: SimpleChanges): void;
}Timing: Wird aufgerufen, bevor
ngOnInit() ausgeführt wird und jedes Mal, wenn sich eine
@Input()-Eigenschaft ändert.
Verwendung: Reaktion auf Änderungen an Eingabewerten.
Beispiel:
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-child',
template: `<p>Vorheriger Wert: {{previousValue}}, Aktueller Wert: {{currentValue}}</p>`
})
export class ChildComponent implements OnChanges {
@Input() value!: string;
previousValue = '';
currentValue = '';
ngOnChanges(changes: SimpleChanges) {
if (changes['value']) {
this.previousValue = changes['value'].previousValue;
this.currentValue = changes['value'].currentValue;
console.log(
`Wert geändert von "${this.previousValue}" zu "${this.currentValue}"`
);
// Prüfen, ob es die erste Änderung ist
if (changes['value'].firstChange) {
console.log('Dies ist die erste Änderung der Eigenschaft!');
}
}
}
}interface OnInit {
ngOnInit(): void;
}Timing: Wird einmal aufgerufen, nachdem die ersten
ngOnChanges() und die Eigenschaftsinitialisierung
abgeschlossen sind.
Verwendung: Hauptinitialisierungslogik, Datenabrufe und -einrichtung.
Beispiel:
import { Component, OnInit, inject } from '@angular/core';
import { UserService } from './user.service';
import { User } from './models/user.model';
@Component({
selector: 'app-user-profile',
template: `
<div *ngIf="user">
<h2>{{user.name}}</h2>
<p>{{user.email}}</p>
</div>
<p *ngIf="errorMessage">{{errorMessage}}</p>
`
})
export class UserProfileComponent implements OnInit {
private userService = inject(UserService);
user: User | null = null;
errorMessage: string = '';
ngOnInit() {
console.log('Komponente wird initialisiert');
this.userService.getCurrentUser().subscribe({
next: (user) => this.user = user,
error: (err) => {
this.errorMessage = 'Benutzer konnte nicht geladen werden';
console.error(err);
}
});
}
}interface DoCheck {
ngDoCheck(): void;
}Timing: Wird bei jedem Erkennungszyklus aufgerufen,
unmittelbar nach ngOnChanges() und
ngOnInit().
Verwendung: Für benutzerdefinierte Änderungserkennung, wenn Angular bestimmte Änderungen nicht erkennt.
Beispiel:
import { Component, DoCheck, Input, KeyValueDiffers, KeyValueDiffer } from '@angular/core';
@Component({
selector: 'app-object-tracker',
template: `<pre>{{changes.join('\n')}}</pre>`
})
export class ObjectTrackerComponent implements DoCheck {
@Input() complexObject: any;
private differ: KeyValueDiffer<string, any>;
changes: string[] = [];
constructor(private differs: KeyValueDiffers) {
// Initialisierung des KeyValueDiffer
}
ngOnInit() {
// Differ erstellen, wenn das Objekt verfügbar ist
if (this.complexObject) {
this.differ = this.differs.find(this.complexObject).create();
}
}
ngDoCheck() {
if (this.differ) {
const changes = this.differ.diff(this.complexObject);
if (changes) {
const changesArray: string[] = [];
changes.forEachChangedItem(item => {
changesArray.push(`Geändert: ${item.key} von ${item.previousValue} zu ${item.currentValue}`);
});
changes.forEachAddedItem(item => {
changesArray.push(`Hinzugefügt: ${item.key} mit Wert ${item.currentValue}`);
});
changes.forEachRemovedItem(item => {
changesArray.push(`Entfernt: ${item.key} (war ${item.previousValue})`);
});
this.changes = [...this.changes, ...changesArray];
}
}
}
}interface AfterContentInit {
ngAfterContentInit(): void;
}Timing: Wird einmal aufgerufen, nachdem Angular externen Inhalt in die Komponentenansicht projiziert hat.
Verwendung: Für die Initialisierung von Elementen,
die über <ng-content> projiziert wurden.
Beispiel:
import { Component, AfterContentInit, ContentChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-content-wrapper',
template: `
<div class="wrapper">
<ng-content></ng-content>
</div>
`
})
export class ContentWrapperComponent implements AfterContentInit {
@ContentChild('projectedHeading') heading?: ElementRef;
ngAfterContentInit() {
if (this.heading) {
console.log('Projizierte Überschrift gefunden:', this.heading.nativeElement.textContent);
// Zugriff auf und Manipulation des projizierten Elements
this.heading.nativeElement.style.color = 'blue';
} else {
console.log('Keine projizierte Überschrift gefunden');
}
}
}
// Verwendung:
// <app-content-wrapper>
// <h2 #projectedHeading>Dies ist eine projizierte Überschrift</h2>
// <p>Weiterer projizierter Inhalt</p>
// </app-content-wrapper>interface AfterContentChecked {
ngAfterContentChecked(): void;
}Timing: Wird nach jedem Durchlauf der Änderungserkennung für den projizierten Inhalt aufgerufen.
Verwendung: Für zusätzliche Prüfungen nach der Änderungserkennung des projizierten Inhalts.
Beispiel:
import { Component, AfterContentChecked, ContentChildren, QueryList } from '@angular/core';
import { TabComponent } from './tab.component';
@Component({
selector: 'app-tabs-container',
template: `
<div class="tabs-header">
<button *ngFor="let tab of tabs" (click)="selectTab(tab)">
{{tab.title}}
</button>
</div>
<div class="tabs-content">
<ng-content></ng-content>
</div>
`
})
export class TabsContainerComponent implements AfterContentChecked {
@ContentChildren(TabComponent) tabs!: QueryList<TabComponent>;
ngAfterContentChecked() {
// Prüfen, ob ein Tab aktiv ist, andernfalls den ersten aktivieren
const activeTabs = this.tabs.filter(tab => tab.active);
if (activeTabs.length === 0 && this.tabs.length > 0) {
// Ersten Tab aktivieren, wenn kein Tab aktiv ist
this.selectTab(this.tabs.first);
}
}
selectTab(tab: TabComponent) {
// Alle Tabs deaktivieren
this.tabs.forEach(tab => tab.active = false);
// Den ausgewählten Tab aktivieren
tab.active = true;
}
}interface AfterViewInit {
ngAfterViewInit(): void;
}Timing: Wird einmal aufgerufen, nachdem Angular die Komponentenansicht und die Ansichten der untergeordneten Komponenten initialisiert hat.
Verwendung: Für Initialisierungslogik, die auf die DOM-Elemente der Komponentenansicht zugreifen muss.
Beispiel:
import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-chart',
template: `<canvas #chartCanvas width="400" height="300"></canvas>`
})
export class ChartComponent implements AfterViewInit {
@ViewChild('chartCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
ngAfterViewInit() {
const canvas = this.canvasRef.nativeElement;
const ctx = canvas.getContext('2d');
if (ctx) {
console.log('Canvas-Kontext verfügbar, Chart wird initialisiert');
// Hier Code für die Chart-Initialisierung
// Beispiel mit hypothetischem ChartJS:
// new Chart(ctx, {
// type: 'bar',
// data: this.chartData,
// options: this.chartOptions
// });
// Einfache Visualisierung zu Demonstrationszwecken
ctx.fillStyle = 'rgba(75, 192, 192, 0.2)';
ctx.fillRect(50, 50, 200, 100);
ctx.strokeStyle = 'rgba(75, 192, 192, 1)';
ctx.strokeRect(50, 50, 200, 100);
}
}
}interface AfterViewChecked {
ngAfterViewChecked(): void;
}Timing: Wird nach jedem Durchlauf der Änderungserkennung für die Komponentenansicht und ihre untergeordneten Komponenten aufgerufen.
Verwendung: Für zusätzliche Prüfungen nach der Änderungserkennung der Ansicht.
Beispiel:
import { Component, AfterViewChecked, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app-form-validator',
template: `
<form #myForm="ngForm">
<input name="username" [(ngModel)]="username" required minlength="3">
<input name="email" [(ngModel)]="email" required email>
<button type="submit" [disabled]="!formValid">Absenden</button>
</form>
`
})
export class FormValidatorComponent implements AfterViewChecked {
@ViewChild('myForm') form!: NgForm;
username = '';
email = '';
formValid = false;
ngAfterViewChecked() {
// Aktualisieren Sie die formValid-Eigenschaft basierend auf dem aktuellen Formularstatus
if (this.form && this.formValid !== this.form.valid) {
// Verwenden Sie setTimeout, um ExpressionChangedAfterItHasBeenCheckedError zu vermeiden
setTimeout(() => {
this.formValid = this.form.valid;
console.log('Formularstatus aktualisiert:', this.formValid);
});
}
}
}interface OnDestroy {
ngOnDestroy(): void;
}Timing: Wird unmittelbar vor der Zerstörung der Komponente durch Angular aufgerufen.
Verwendung: Für Aufräumarbeiten wie das Abmelden von Observables, Eventlistenern oder anderen Ressourcen.
Beispiel:
import { Component, OnDestroy } from '@angular/core';
import { Subject, interval, takeUntil } from 'rxjs';
@Component({
selector: 'app-countdown',
template: `<h3>Countdown: {{counter}}</h3>`
})
export class CountdownComponent implements OnDestroy {
private destroy$ = new Subject<void>();
counter = 10;
constructor() {
// Starten eines Intervalls, das alle Sekunde aktualisiert wird
interval(1000).pipe(
takeUntil(this.destroy$) // Abonnement beenden, wenn destroy$ ausgelöst wird
).subscribe(() => {
this.counter--;
if (this.counter < 0) this.counter = 10;
});
}
ngOnDestroy() {
console.log('Komponente wird zerstört, Ressourcen werden freigegeben');
// Subject auslösen, um alle Abonnements zu beenden
this.destroy$.next();
this.destroy$.complete();
// Andere Aufräumarbeiten
// z.B. clearTimeout, clearInterval, Event-Listener entfernen
}
}Mit der Einführung der Signal API in neueren Angular-Versionen hat sich auch die Art und Weise verändert, wie Lifecycle Hooks mit reaktiven Daten interagieren:
import { Component, OnInit, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-signal-demo',
template: `
<div>
<h2>Zähler: {{counter()}}</h2>
<p>Quadrat: {{squared()}}</p>
<button (click)="increment()">Erhöhen</button>
</div>
`
})
export class SignalDemoComponent implements OnInit {
counter = signal(0); // Signal erstellen
squared = computed(() => this.counter() * this.counter()); // Computed-Signal
ngOnInit() {
// Effect erstellen, der bei Änderungen des Signals ausgeführt wird
effect(() => {
console.log(`Counter wurde auf ${this.counter()} aktualisiert`);
});
}
increment() {
this.counter.update(val => val + 1);
}
// Der Effect wird automatisch bereinigt, wenn die Komponente zerstört wird
}Verwendung von Signals vs. ngOnChanges: In
komplexen Szenarien, bei denen Reaktivität wichtig ist, können Signals
eine Alternative zu ngOnChanges sein.
ngOnInit für Initialisierung: Verwenden Sie
ngOnInit für einmalige Initialisierungen, wie HTTP-Anfragen
oder komplexe Berechnungen.
Sparsame Verwendung von ngDoCheck: Verwenden Sie
ngDoCheck sparsam, da es Leistungsprobleme verursachen
kann.
Verwenden Sie ngOnDestroy: Stellen Sie sicher,
dass alle Ressourcen in ngOnDestroy bereinigt werden, um
Speicherlecks zu vermeiden.
Vermeiden Sie DOM-Manipulation in
ngAfterViewInit: Beachten Sie, dass DOM-Manipulationen in
ngAfterViewInit einen
ExpressionChangedAfterItHasBeenCheckedError auslösen
können. Verwenden Sie setTimeout oder die
afterNextRender-API (in neueren Versionen), um dieses
Problem zu vermeiden.
Lifecycle Hooks sind ein mächtiges Werkzeug in Angular, das uns ermöglicht, zu bestimmten Zeitpunkten im Leben einer Komponente einzugreifen. Mit neueren Funktionen wie Signals sind Lifecycle Hooks noch leistungsfähiger und effizienter geworden. Indem wir verstehen, wie und wann diese Hooks ausgeführt werden, können wir robustere und effektivere Angular-Anwendungen erstellen.