Komponenten sind nicht statisch. Sie werden erstellt, konfiguriert, updated, und schließlich destroyed. Während dieser Phasen müssen verschiedene Operations ausgeführt werden. Beim Erstellen müssen Daten geladen werden. Bei Input-Changes müssen Berechnungen neu ausgeführt werden. Beim Destroy müssen Subscriptions cleaned werden.
Ohne strukturierte Hooks wäre dies chaotisch. Wo würde man Initialization-Code platzieren? Wann wäre der richtige Zeitpunkt für DOM-Manipulations? Wie würde man Cleanup orchestrieren? Angular’s Lifecycle Hooks lösen dieses Problem durch well-defined Callback-Points.
Lifecycle Hooks sind Interface-Methods die Components implementieren. Angular callt diese Methods zu spezifischen Zeitpunkten. Der Contract ist einfach: implementiere das Interface, Angular callt die Method zur richtigen Zeit.
Der Constructor läuft vor allen Lifecycle Hooks. Er ist TypeScript/JavaScript, nicht Angular-spezifisch. Dependency Injection passiert im Constructor. Property-Initialization passiert im Constructor. Aber: Complex Logic gehört nicht in den Constructor.
@Component({
selector: 'app-user',
template: `<div>{{ userName }}</div>`
})
export class UserComponent {
userName: string;
constructor(private userService: UserService) {
// Gut: Dependencies injecten
// Gut: Simple Property-Initialization
this.userName = 'Loading...';
// SCHLECHT: HTTP-Requests
// this.userService.getUser().subscribe(...); // Gehört in ngOnInit!
// SCHLECHT: DOM-Access
// document.querySelector('...'); // DOM existiert noch nicht!
}
}Im Constructor ist die Component noch nicht vollständig initialized.
@Input Properties sind noch nicht gesetzt. Child-Components
existieren noch nicht. DOM ist nicht rendered. Der Constructor ist zu
früh für die meisten Operations.
ngOnChanges läuft immer wenn @Input
Properties ändern. Es läuft vor ngOnInit (für initial
values) und dann bei jedem Input-Change. Die Method erhält ein
SimpleChanges Objekt mit allen Changes.
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-product',
template: `
<div class="product">
<h2>{{ product.name }}</h2>
<p>Price: {{ product.price }}</p>
@if (priceChanged) {
<span class="badge">Price changed!</span>
}
</div>
`
})
export class ProductComponent implements OnChanges {
@Input() product: Product;
@Input() discount: number;
priceChanged = false;
effectivePrice: number;
ngOnChanges(changes: SimpleChanges) {
// Check welche Properties geändert haben
if (changes['product']) {
const change = changes['product'];
console.log('Previous:', change.previousValue);
console.log('Current:', change.currentValue);
console.log('First change:', change.firstChange);
// Detect price changes (nicht beim ersten Mal)
if (!change.firstChange) {
const oldPrice = change.previousValue?.price;
const newPrice = change.currentValue?.price;
if (oldPrice !== newPrice) {
this.priceChanged = true;
setTimeout(() => this.priceChanged = false, 3000);
}
}
}
// Recalculate wenn product oder discount ändern
if (changes['product'] || changes['discount']) {
this.calculateEffectivePrice();
}
}
private calculateEffectivePrice() {
this.effectivePrice = this.product.price * (1 - (this.discount || 0));
}
}Das SimpleChanges Object ist ein Dictionary. Keys sind
Property-Namen. Values sind SimpleChange Objects mit
previousValue, currentValue und
firstChange.
firstChange ist true beim initialen Run.
Dies ist wichtig – oft will man Logic nur bei späteren Changes, nicht
beim Initial-Setup. Beispiel: “Show notification when price changes”
sollte nicht beim ersten Render triggern.
ngOnChanges läuft nur für @Input
Properties. Local Component-State triggert es nicht. Wenn
this.localProperty = newValue, läuft
ngOnChanges nicht. Dies macht Sinn –
ngOnChanges tracked Parent→Child Data-Flow.
ngOnInit läuft einmal, nach dem ersten
ngOnChanges. Alle @Input Properties sind
gesetzt. Dependencies sind injected. Dies ist der primäre Hook für
Initialization-Logic.
import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-user-detail',
template: `
@if (loading) {
<div class="spinner">Loading...</div>
}
@if (user) {
<div class="user-detail">
<h1>{{ user.name }}</h1>
<p>{{ user.email }}</p>
<p>{{ user.bio }}</p>
</div>
}
@if (error) {
<div class="error">{{ error }}</div>
}
`
})
export class UserDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private userService = inject(UserService);
user: User | null = null;
loading = false;
error: string | null = null;
ngOnInit() {
// Route-Parameter lesen
const userId = this.route.snapshot.paramMap.get('id');
if (!userId) {
this.error = 'No user ID provided';
return;
}
// HTTP-Request
this.loading = true;
this.userService.getUser(userId).subscribe({
next: (user) => {
this.user = user;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load user';
this.loading = false;
console.error(err);
}
});
}
}ngOnInit ist der richtige Ort für: - HTTP-Requests -
Complex Calculations basierend auf @Input Values -
Subscriptions zu Observables - Route-Parameter-Reading -
Local-State-Initialization
Ein häufiger Fehler ist Logic im Constructor statt
ngOnInit. Beispiel: HTTP-Request im Constructor. Problem:
@Input Properties sind noch nicht gesetzt. Wenn die
Component @Input() userId: string hat, ist
userId im Constructor undefined.
ngOnInit läuft nur einmal. Wenn @Input
Properties später ändern, läuft ngOnInit nicht erneut.
Dafür gibt es ngOnChanges.
ngDoCheck läuft bei jedem Change Detection Cycle. Dies
ist after ngOnChanges und ngOnInit, und dann
bei jedem weiteren Cycle. Es ist ein Hook für custom Change Detection
Logic.
Angular’s Default Change Detection detected nur Referenz-Changes für
Objects. Wenn ein Object mutiert wird (property ändert, aber Referenz
bleibt), detected Angular dies nicht automatisch. ngDoCheck
ermöglicht custom Detection-Logic.
import { Component, Input, DoCheck, KeyValueDiffers, KeyValueDiffer } from '@angular/core';
@Component({
selector: 'app-config-viewer',
template: `
<div class="config">
<h3>Configuration</h3>
<pre>{{ config | json }}</pre>
@if (changes.length > 0) {
<div class="changes">
<h4>Recent Changes:</h4>
<ul>
@for (change of changes; track change) {
<li>{{ change }}</li>
}
</ul>
</div>
}
</div>
`
})
export class ConfigViewerComponent implements DoCheck {
@Input() config: any;
private differ: KeyValueDiffer<string, any>;
changes: string[] = [];
constructor(private differs: KeyValueDiffers) {}
ngOnInit() {
// Create differ für das config object
if (this.config) {
this.differ = this.differs.find(this.config).create();
}
}
ngDoCheck() {
if (this.differ) {
const changes = this.differ.diff(this.config);
if (changes) {
changes.forEachChangedItem((record) => {
this.changes.push(
`${record.key}: ${record.previousValue} → ${record.currentValue}`
);
});
changes.forEachAddedItem((record) => {
this.changes.push(`Added ${record.key}: ${record.currentValue}`);
});
changes.forEachRemovedItem((record) => {
this.changes.push(`Removed ${record.key}`);
});
// Keep nur last 10 changes
if (this.changes.length > 10) {
this.changes = this.changes.slice(-10);
}
}
}
}
}KeyValueDiffer ist Angular’s Utility für deep
Object-Comparison. Es tracked Additions, Removals und Changes von
Object-Properties. Ohne ngDoCheck und Differs müsste man
manuell deep-compare Objects.
Warnung: ngDoCheck läuft sehr oft. Bei
jedem Change Detection Cycle. Heavy Logic in ngDoCheck
killt Performance. Use it sparingly. Für die meisten Cases ist OnPush +
Immutability besser als custom Change Detection.
ngAfterContentInit läuft einmal, nachdem
Content-Projection initialized ist. Content-Projection ist
<ng-content> – HTML das von außen in die Component
injected wird.
import { Component, AfterContentInit, ContentChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="card-header">
<ng-content select="[card-title]"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content>
</div>
</div>
`,
styles: [`
.card { border: 1px solid #ddd; }
.card-header { background: #f5f5f5; padding: 1rem; }
.card-body { padding: 1rem; }
`]
})
export class CardComponent implements AfterContentInit {
@ContentChild('cardTitle') titleElement?: ElementRef;
ngAfterContentInit() {
if (this.titleElement) {
console.log('Card title:', this.titleElement.nativeElement.textContent);
// Man kann projected content manipulieren
// Aber: Vorsicht mit ExpressionChangedAfterItHasBeenCheckedError
this.titleElement.nativeElement.style.fontWeight = 'bold';
} else {
console.log('No title provided');
}
}
}
// Usage:
// <app-card>
// <h2 card-title #cardTitle>My Card</h2>
// <p>Card content here</p>
// </app-card>@ContentChild queried projected Content. Es ist
undefined bis ngAfterContentInit. Vorher
existiert der projected Content nicht – daher der Hook.
@ContentChildren ist die plural Version für
QueryLists:
@Component({
selector: 'app-tabs',
template: `
<div class="tabs-header">
@for (tab of tabs; track tab) {
<button (click)="selectTab(tab)"
[class.active]="tab.active">
{{ tab.title }}
</button>
}
</div>
<div class="tabs-content">
<ng-content></ng-content>
</div>
`
})
export class TabsComponent implements AfterContentInit {
@ContentChildren(TabComponent) tabs: QueryList<TabComponent>;
ngAfterContentInit() {
// Activate first tab by default
if (this.tabs.length > 0 && !this.tabs.find(t => t.active)) {
this.tabs.first.active = true;
}
// Listen to changes in tabs
this.tabs.changes.subscribe(() => {
console.log('Tabs changed, count:', this.tabs.length);
});
}
selectTab(selectedTab: TabComponent) {
this.tabs.forEach(tab => tab.active = false);
selectedTab.active = true;
}
}Die QueryList ist reactive. Die changes
Observable emittiert wenn projected Content added/removed wird. Dies
ermöglicht dynamic Content-Handling.
ngAfterContentChecked läuft nach jedem Change Detection
Cycle für projected Content. Dies ist analog zu ngDoCheck,
aber speziell für Content-Projection.
@Component({
selector: 'app-accordion',
template: `
<div class="accordion">
<ng-content></ng-content>
</div>
`
})
export class AccordionComponent implements AfterContentChecked {
@ContentChildren(AccordionItemComponent) items: QueryList<AccordionItemComponent>;
ngAfterContentChecked() {
// Ensure only one item is expanded
const expandedItems = this.items.filter(item => item.expanded);
if (expandedItems.length > 1) {
// Close all except first
expandedItems.slice(1).forEach(item => item.expanded = false);
}
}
}Warnung: Wie ngDoCheck läuft
ngAfterContentChecked sehr oft. Avoid heavy Logic. Use it
für Validations oder Consistency-Checks, nicht für expensive
Operations.
ngAfterViewInit läuft einmal, nachdem die Component’s
View und alle Child-Components initialized sind. Dies ist der früheste
Zeitpunkt wo DOM-Elements sicher verfügbar sind.
import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<div class="chart-container">
<canvas #chartCanvas></canvas>
</div>
`
})
export class ChartComponent implements AfterViewInit {
@ViewChild('chartCanvas') canvas: ElementRef<HTMLCanvasElement>;
ngAfterViewInit() {
// DOM ist verfügbar, Canvas existiert
const ctx = this.canvas.nativeElement.getContext('2d');
if (ctx) {
// Initialize Chart.js oder andere Charting-Library
this.initializeChart(ctx);
}
}
private initializeChart(ctx: CanvasRenderingContext2D) {
// Chart-Initialization mit Third-Party-Library
// z.B. new Chart(ctx, { ... })
// Simple Demo-Drawing
ctx.fillStyle = '#4CAF50';
ctx.fillRect(50, 50, 200, 100);
}
}@ViewChild queried die Component’s eigene
Template-Elements. Es ist undefined bis
ngAfterViewInit. Vorher ist die View nicht rendered.
Ein common Use-Case ist Integration von Third-Party-Libraries die DOM-Access benötigen:
@Component({
selector: 'app-map',
template: `<div #mapContainer class="map"></div>`
})
export class MapComponent implements AfterViewInit, OnDestroy {
@ViewChild('mapContainer') mapContainer: ElementRef;
private map: any; // Leaflet/Google Maps instance
ngAfterViewInit() {
// Initialize map library
this.map = new LeafletMap(this.mapContainer.nativeElement, {
center: [51.505, -0.09],
zoom: 13
});
}
ngOnDestroy() {
// Cleanup map instance
if (this.map) {
this.map.remove();
}
}
}Wichtig: DOM-Manipulation in
ngAfterViewInit kann
ExpressionChangedAfterItHasBeenCheckedError triggern.
Angular hat bereits Change Detection für diese View durchgeführt. Wenn
man dann Properties ändert die im Template gebunden sind, detektiert
Angular einen Inconsistency.
Solution: Wrap State-Changes in setTimeout oder nutze
afterNextRender:
ngAfterViewInit() {
// Measure DOM element size
const height = this.element.nativeElement.offsetHeight;
// BAD: Direct assignment triggers error in development
// this.componentHeight = height;
// GOOD: Defer to next tick
setTimeout(() => {
this.componentHeight = height;
});
// BETTER (Angular 16+): Use afterNextRender
afterNextRender(() => {
this.componentHeight = height;
});
}ngAfterViewChecked läuft nach jedem Change Detection
Cycle für die View. Dies ist after ngAfterViewInit und dann
bei jedem weiteren Cycle.
@Component({
selector: 'app-scroll-tracker',
template: `
<div class="content" #scrollContent>
<p *ngFor="let item of items">{{ item }}</p>
</div>
<div class="indicator">
Scroll Position: {{ scrollPosition }}
</div>
`
})
export class ScrollTrackerComponent implements AfterViewChecked {
@ViewChild('scrollContent') scrollElement: ElementRef;
items = Array.from({ length: 100 }, (_, i) => `Item ${i}`);
scrollPosition = 0;
ngAfterViewChecked() {
// Check scroll position after each render
if (this.scrollElement) {
const element = this.scrollElement.nativeElement;
const newPosition = element.scrollTop;
// Only update if changed to avoid infinite loop
if (newPosition !== this.scrollPosition) {
setTimeout(() => {
this.scrollPosition = newPosition;
});
}
}
}
}Warnung: ngAfterViewChecked läuft sehr
oft. Jeder Change Detection Cycle. Heavy Logic hier destroyed
Performance. Use sparingly, nur für lightweight Checks.
ngOnDestroy läuft unmittelbar bevor Angular die
Component destroyed. Dies ist der Ort für Cleanup – Subscriptions
beenden, Timers clearen, Event-Listeners entfernen.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, interval, takeUntil } from 'rxjs';
@Component({
selector: 'app-timer',
template: `
<div class="timer">
<h2>{{ seconds }}s</h2>
<button (click)="start()">Start</button>
<button (click)="stop()">Stop</button>
</div>
`
})
export class TimerComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
seconds = 0;
ngOnInit() {
// Subscription die automatisch cleaned wird
interval(1000).pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.seconds++;
});
}
ngOnDestroy() {
// Trigger destroy$ to complete all subscriptions
this.destroy$.next();
this.destroy$.complete();
console.log('TimerComponent destroyed, subscriptions cleaned');
}
start() {
// Implementation
}
stop() {
// Implementation
}
}Das destroy$ Subject Pattern ist common für
Observable-Cleanup. Alle Subscriptions nutzen
takeUntil(destroy$). Ein Single
destroy$.next() in ngOnDestroy completed alle
Subscriptions.
Ohne Cleanup entstehen Memory-Leaks. Subscriptions bleiben aktiv auch wenn die Component nicht mehr existiert. Bei vielen Component-Creations/Destructions akkumulieren sich Subscriptions und konsumieren Memory und CPU.
// BAD: Memory leak
export class BadComponent implements OnInit {
ngOnInit() {
// Subscription wird nie beendet
this.dataService.getData().subscribe(data => {
console.log(data);
});
}
}
// GOOD: Proper cleanup
export class GoodComponent implements OnInit, OnDestroy {
private subscription: Subscription;
ngOnInit() {
this.subscription = this.dataService.getData().subscribe(data => {
console.log(data);
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
// BETTER: Subject-based cleanup
export class BetterComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.dataService.getData()
.pipe(takeUntil(this.destroy$))
.subscribe(data => console.log(data));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}Angular’s AsyncPipe handlet Cleanup automatisch. Wenn
eine Component mit | async destroyed wird, unsubscribes die
Pipe automatisch:
@Component({
template: `
<div *ngIf="data$ | async as data">
{{ data.name }}
</div>
`
})
export class AutoCleanupComponent {
// Kein ngOnDestroy nötig - AsyncPipe cleaned automatisch
data$ = this.dataService.getData();
constructor(private dataService: DataService) {}
}Angular Signals introduced ein alternatives Reactivity-Model. Effects sind ähnlich zu Lifecycle Hooks aber reaktiver:
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<h2>Count: {{ count() }}</h2>
<button (click)="increment()">+</button>
</div>
`
})
export class CounterComponent {
count = signal(0);
constructor() {
// Effect läuft bei jeder Signal-Änderung
effect(() => {
console.log('Count changed to:', this.count());
// Side-effects basierend auf Signal-Value
if (this.count() > 10) {
console.log('Count exceeded 10!');
}
});
}
increment() {
this.count.update(n => n + 1);
}
// Effects werden automatisch cleaned bei Component-Destroy
// Kein manuelles ngOnDestroy nötig
}Effects cleaned automatisch. Man muss nicht manuell unsubscribe. Dies vereinfacht Code erheblich. Der Effect läuft initial und dann bei jeder Änderung der genutzten Signals.
Für async Operations kombiniert man Signals mit
toObservable:
import { Component, signal, effect } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { switchMap } from 'rxjs/operators';
@Component({
selector: 'app-user-search',
template: `
<input [value]="searchTerm()"
(input)="searchTerm.set($event.target.value)">
@if (results(); as users) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
}
`
})
export class UserSearchComponent {
searchTerm = signal('');
results = signal<User[]>([]);
constructor() {
// Convert signal to observable, dann async operation
toObservable(this.searchTerm).pipe(
debounceTime(300),
switchMap(term => this.userService.search(term))
).subscribe(users => {
this.results.set(users);
});
}
}Dies kombiniert Signals’ Simplicity mit Observables’ Power für async Operations.
Die Reihenfolge der Hooks ist garantiert und wichtig:
| Hook | Wann | Häufigkeit | Verwendung |
|---|---|---|---|
| Constructor | Vor allem | Einmal | DI, simple Init |
| ngOnChanges | Input-Change | Mehrmals | Input-Tracking |
| ngOnInit | Nach Init | Einmal | Main-Initialization |
| ngDoCheck | Change-Detection | Sehr oft | Custom CD |
| ngAfterContentInit | Content-Init | Einmal | Content-Access |
| ngAfterContentChecked | Content-CD | Sehr oft | Content-Validation |
| ngAfterViewInit | View-Init | Einmal | DOM-Access |
| ngAfterViewChecked | View-CD | Sehr oft | DOM-Validation |
| ngOnDestroy | Vor Destroy | Einmal | Cleanup |
Bei Parent-Child-Relationships ist die Reihenfolge: 1. Parent Constructor 2. Parent ngOnInit 3. Child Constructor 4. Child ngOnInit 5. Child ngAfterViewInit 6. Parent ngAfterViewInit
Dies macht Sinn – Children müssen initialized sein bevor Parent’s View complete ist.
@Component({
selector: 'app-heavy-component',
template: `...`
})
export class HeavyComponent implements OnInit {
data: any[] = [];
ngOnInit() {
// Load only when component is actually created
this.loadHeavyData();
}
private loadHeavyData() {
this.dataService.getHeavyData().subscribe(data => {
this.data = data;
});
}
}Dies ist besser als Constructor-Loading – die Component wird nur loaded wenn sie tatsächlich gerendert wird.
// BAD: Performance-Killer
ngDoCheck() {
// Expensive calculation bei jedem CD-Cycle
this.computedValue = this.expensiveCalculation();
}
// GOOD: Memoize oder nutze computed Signals
private lastInput: any;
private cachedResult: any;
ngDoCheck() {
if (this.input !== this.lastInput) {
this.cachedResult = this.expensiveCalculation();
this.lastInput = this.input;
}
}
// BETTER: Use computed signal
computed = computed(() => this.expensiveCalculation(this.input()));// BAD: Memory leak
export class LeakyComponent implements OnInit {
ngOnInit() {
this.dataService.stream$.subscribe(data => {
this.data = data;
});
}
}
// GOOD: Proper cleanup
export class CleanComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.dataService.stream$
.pipe(takeUntil(this.destroy$))
.subscribe(data => this.data = data);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}Lifecycle Hooks sind Angular’s Mechanism für
Component-Lifecycle-Management. Sie bieten precise Control-Points für
Initialization, Change-Handling und Cleanup. Der Constructor läuft first
aber ist zu früh für most Logic. ngOnInit ist der primary
Initialization-Hook. ngOnDestroy ist critical für
Resource-Cleanup. Die Content- und View-Hooks ermöglichen
DOM-Manipulation zu safe Timepoints. Signals und Effects bieten ein
moderneres, einfacheres Reactivity-Model das viele
Manual-Lifecycle-Patterns obsolet macht. Understanding Lifecycle-Timing
ist essential für Bug-free, performant Angular-Applications.