Eine Angular-Komponente ist kein statisches Objekt, sondern ein Lebewesen mit Phasen: Sie wird instanziiert, bekommt Inputs, rendert ihr Template, projiziert Inhalte, malt das DOM, reagiert auf Änderungen — und wird am Ende wieder zerstört. Für jede dieser Phasen bietet Angular Lifecycle-Hooks: Methoden, die das Framework zu definierten Zeitpunkten aufruft.
Der klassische Stack — ngOnInit, ngOnChanges, ngAfterViewInit, ngOnDestroy — ist nach wie vor gültig. Daneben existieren mit afterRender, afterNextRender und DestroyRef modernere Werkzeuge, die in einer Signal-getriebenen, Zoneless-fähigen Welt deutlich besser passen. Dieser Artikel zeigt dir alle Hooks, ihre Reihenfolge, ihre typischen Use-Cases — und wann du heute lieber zu Signals, effect() oder afterNextRender greifst.
Phasen, Hooks, Interfaces
Der Lifecycle beschreibt die Phasen, die jede Component-Instanz durchläuft: Erstellung im Constructor, erste Input-Belegung, Template-Render, View-Init, n-fache Change-Detection-Zyklen, schließlich Destroy. Angular signalisiert jeden Übergang über einen optionalen Hook — eine Methode mit festem Namen (ngOnInit, ngOnDestroy etc.), die du auf der Klasse implementierst.
Für jeden Hook existiert ein zugehöriges TypeScript-Interface (OnInit, OnDestroy, OnChanges …). Das Implementieren ist optional — Angular ruft die Methode auch ohne implements-Klausel — aber dringend empfohlen, weil der Compiler dann Schreibfehler erkennt.
import {
Component, Input,
OnChanges, OnInit, DoCheck,
AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked,
OnDestroy, SimpleChanges,
} from '@angular/core';
@Component({
selector: 'app-lifecycle-demo',
standalone: true,
template: `<p>{{ label }}</p>`,
})
export class LifecycleDemoComponent
implements OnChanges, OnInit, DoCheck,
AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked,
OnDestroy {
@Input() label = '';
constructor() { console.log('1. constructor'); }
ngOnChanges(changes: SimpleChanges) { console.log('2. ngOnChanges', changes); }
ngOnInit() { console.log('3. ngOnInit'); }
ngDoCheck() { console.log('4. ngDoCheck'); }
ngAfterContentInit() { console.log('5. ngAfterContentInit'); }
ngAfterContentChecked() { console.log('6. ngAfterContentChecked'); }
ngAfterViewInit() { console.log('7. ngAfterViewInit'); }
ngAfterViewChecked() { console.log('8. ngAfterViewChecked'); }
ngOnDestroy() { console.log('9. ngOnDestroy'); }
}Alle acht Hooks tabellarisch
| Hook | Zeitpunkt | Typischer Use-Case |
|---|---|---|
ngOnChanges | Vor ngOnInit und bei jeder Input-Änderung | Auf neue Input-Werte reagieren (klassisch) |
ngOnInit | Einmalig nach dem ersten ngOnChanges | Initiale Daten laden, Felder ableiten |
ngDoCheck | Bei JEDER Change-Detection | Custom-Change-Detection (selten nötig) |
ngAfterContentInit | Einmalig nach erstem Content-Projection-Check | @ContentChild-Resultate lesen |
ngAfterContentChecked | Nach jedem Content-Check | Auf projizierten Inhalt reagieren |
ngAfterViewInit | Einmalig nach erstem View-Check | @ViewChild-Resultate, DOM-Init |
ngAfterViewChecked | Nach jedem View-Check | View-Verifizierung (selten) |
ngOnDestroy | Direkt vor Component-Zerstörung | Cleanup: unsubscribe, clearInterval, removeEventListener |
Initial-Setup nach Input-Bindung
ngOnInit ist der mit Abstand am häufigsten verwendete Hook. Er läuft einmal, direkt nach dem ersten ngOnChanges — also nachdem alle @Input()-Properties initial gebunden wurden. Im Constructor sind diese (bei der klassischen Decorator-API) noch undefined.
import { Component, Input, OnInit, inject } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user-detail',
standalone: true,
template: `<p>{{ user?.name }}</p>`,
})
export class UserDetailComponent implements OnInit {
@Input() userId!: string;
private userService = inject(UserService);
user: { name: string } | null = null;
constructor() {
// FALSCH: this.userId ist hier noch undefined.
console.log(this.userId); // undefined
}
ngOnInit() {
// RICHTIG: Inputs sind jetzt gesetzt.
this.userService.load(this.userId).subscribe((u) => (this.user = u));
}
}SimpleChanges, firstChange, currentValue
ngOnChanges bekommt ein SimpleChanges-Objekt — eine Map mit einem Eintrag pro geändertem @Input(). Jeder Eintrag enthält previousValue, currentValue und firstChange (true beim ersten Lauf).
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-filter-panel',
standalone: true,
template: `<p>Aktiver Filter: {{ filter }}</p>`,
})
export class FilterPanelComponent implements OnChanges {
@Input() filter = '';
@Input() page = 1;
ngOnChanges(changes: SimpleChanges) {
if (changes['filter'] && !changes['filter'].firstChange) {
console.log(
'Filter geändert:',
changes['filter'].previousValue,
'→',
changes['filter'].currentValue
);
// Reset Pagination, sobald Filter wechselt
this.page = 1;
}
}
}View vs. Content — und Signal-Queries als Ablöse
Der Unterschied ist konzeptuell einfach:
- View = das eigene Template der Komponente, also alles im
template-Feld. - Content = projizierter Inhalt zwischen den Component-Tags, gelesen über
<ng-content>.
ngAfterContentInit läuft, sobald die projizierten Kinder initialisiert sind; ngAfterViewInit läuft, sobald das eigene Template inklusive Child-Components fertig initialisiert ist. Erst dann sind @ViewChild/@ContentChild-Refs gesetzt.
import {
Component, AfterViewInit, ViewChild, ElementRef, viewChild,
} from '@angular/core';
@Component({
selector: 'app-modal',
standalone: true,
template: `
<div class="modal" #modal>
<h2>Titel</h2>
<button #closeBtn>X</button>
</div>
`,
})
export class ModalComponent implements AfterViewInit {
// Klassisch: erst nach ngAfterViewInit verfügbar.
@ViewChild('closeBtn') closeBtn!: ElementRef<HTMLButtonElement>;
// Modern: Signal-Query — auto-tracked, kein Hook nötig.
modalRef = viewChild<ElementRef<HTMLElement>>('modal');
ngAfterViewInit() {
// Klassisch: Fokus auf Schließen-Button setzen.
this.closeBtn.nativeElement.focus();
}
// Modern, Alternative: über effect() reagieren, sobald die Query auflöst.
// constructor() {
// effect(() => this.modalRef()?.nativeElement.focus());
// }
}Aufräumen vor dem Tod der Komponente
ngOnDestroy läuft direkt vor der Zerstörung der Komponente — die Stelle, an der du Subscriptions kündigst, Timer killst und EventListener entfernst. Vergisst du das, hast du einen Memory-Leak: die Komponente verschwindet aus dem DOM, aber ihr Closure-State bleibt am setInterval oder Subscription hängen.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { interval, Subscription } from 'rxjs';
@Component({
selector: 'app-ticker',
standalone: true,
template: `<p>Tick: {{ tick }}</p>`,
})
export class TickerComponent implements OnInit, OnDestroy {
tick = 0;
private intervalId?: number;
private sub?: Subscription;
ngOnInit() {
this.intervalId = window.setInterval(() => this.tick++, 1000);
this.sub = interval(500).subscribe(() => console.log('rx tick'));
}
ngOnDestroy() {
clearInterval(this.intervalId);
this.sub?.unsubscribe();
}
}inject(DestroyRef) und takeUntilDestroyed
DestroyRef (seit v16) ist die funktionale Alternative zu ngOnDestroy. Du injizierst sie und registrierst Cleanup-Callbacks per onDestroy(fn). Der entscheidende Vorteil: Cleanup ist lokal dort definiert, wo die Ressource entsteht — nicht am anderen Ende der Klasse in einem separaten Hook.
import { Component, DestroyRef, inject } from '@angular/core';
import { interval } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-destroy-ref',
standalone: true,
template: `<p>Tick: {{ tick }}</p>`,
})
export class DestroyRefComponent {
private destroyRef = inject(DestroyRef);
tick = 0;
constructor() {
// 1) Klassisches Cleanup, lokal definiert.
const id = window.setInterval(() => this.tick++, 1000);
this.destroyRef.onDestroy(() => clearInterval(id));
// 2) takeUntilDestroyed — RxJS-Interop ohne ngOnDestroy.
interval(500)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => console.log('rx tick'));
}
}DOM-Lifecycle nach dem Malen
Während ngAfterViewInit nach dem Erzeugen des Views feuert, laufen afterRender und afterNextRender (stable seit v17) erst, nachdem das DOM tatsächlich gemalt wurde. Der Use-Case: DOM-Messungen, Drittanbieter-Bibliotheken (Charts, Maps, Tooltips), Layout-Berechnungen.
afterNextRender(fn)läuft einmal beim nächsten Render. Ideal für initialen Setup.afterRender(fn)läuft bei jedem Render. Selten nötig.
Beide unterstützen vier Phasen, die Layout-Thrashing minimieren:
| Phase | Zweck |
|---|---|
earlyRead | DOM lesen, BEVOR geschrieben wird |
write | DOM schreiben (Styles, Attribute) |
mixedReadWrite | Default — nur als Fallback |
read | DOM lesen, NACHDEM alle Writes durch sind |
import { Component, ElementRef, afterNextRender, viewChild } from '@angular/core';
@Component({
selector: 'app-chart-mount',
standalone: true,
template: `<div #host class="chart"></div>`,
})
export class ChartMountComponent {
host = viewChild<ElementRef<HTMLDivElement>>('host');
constructor() {
afterNextRender({
write: () => {
// DOM ist gemalt — Drittanbieter-Lib initialisieren.
const el = this.host()?.nativeElement;
if (el) initChartLibrary(el);
},
read: () => {
// Maße lesen, ohne danach zu schreiben → kein Re-Layout.
console.log('Chart-Höhe:', this.host()?.nativeElement.offsetHeight);
},
});
}
}
declare function initChartLibrary(el: HTMLElement): void;OnPush, Zoneless, Hook-Frequenz
Die Hooks laufen, was viele unterschätzen, abhängig von der Change-Detection-Strategie:
- Default:
ngDoCheck,ngAfterContentChecked,ngAfterViewCheckedlaufen pro Component und Cycle — schnell mehrfach pro Sekunde. - OnPush: Sie laufen nur, wenn die Komponente auf einem CD-Pfad liegt (Input-Change, Event, Async-Pipe-Emit, manuelles
markForCheck). - Zoneless: Ohne Zone gibt es keinen impliziten Tick — die Checked-Hooks feuern nur, wenn ein Signal liest oder ein expliziter Trigger das CD anstößt.
Daraus folgt eine klare Faustregel: Für moderne Apps brauchst du ngDoCheck praktisch nie. Wenn du custom-Reaktionen auf Werte-Änderungen brauchst, ist effect() auf einem Signal die saubere Antwort. Wenn du Render-Aktivität brauchst, ist afterRender der präzise Hook.
Vollständiges Beispiel mit afterNextRender + DestroyRef + Signals
Eine Tooltip-Komponente, die nach dem Mount ihre Position relativ zum Trigger berechnet, einen Outside-Click-Listener registriert und beim Destroy alles aufräumt — alles ohne klassische Hooks:
import {
Component, ElementRef, DestroyRef, inject,
afterNextRender, signal, input, viewChild,
} from '@angular/core';
@Component({
selector: 'app-tooltip',
standalone: true,
template: `
<div #panel class="tooltip" [style.top.px]="pos().top" [style.left.px]="pos().left">
{{ text() }}
</div>
`,
})
export class TooltipComponent {
// Signal-Inputs — verfügbar ab Constructor.
text = input.required<string>();
anchor = input.required<HTMLElement>();
private destroyRef = inject(DestroyRef);
private panel = viewChild<ElementRef<HTMLElement>>('panel');
pos = signal({ top: 0, left: 0 });
constructor() {
afterNextRender({
read: () => {
// DOM ist gemalt: Maße lesen und Position berechnen.
const a = this.anchor().getBoundingClientRect();
const p = this.panel()?.nativeElement.getBoundingClientRect();
if (!p) return;
this.pos.set({
top: a.bottom + 8,
left: a.left + a.width / 2 - p.width / 2,
});
},
});
// Outside-Click → Cleanup über DestroyRef.
const onClick = (e: MouseEvent) => {
if (!this.panel()?.nativeElement.contains(e.target as Node)) {
// Hier z. B. ein closed-Output emittieren.
}
};
document.addEventListener('click', onClick);
this.destroyRef.onDestroy(() => document.removeEventListener('click', onClick));
}
}Das Resultat: kein einziger ng*-Hook, vollständige Cleanup-Garantie, Signal-getriebenes State-Management. So sieht moderner Angular-Code 2026 aus.
Entscheidungsmatrix
| Aufgabe | Klassisch | Modern |
|---|---|---|
| Initial-Daten laden | ngOnInit | Constructor + Signal-Init / resource() |
| Auf Input-Änderung reagieren | ngOnChanges | input() + effect() / computed() |
@ViewChild lesen | ngAfterViewInit | viewChild() (Signal-Query) |
| Cleanup beim Destroy | ngOnDestroy | inject(DestroyRef).onDestroy(...) |
| RxJS unsubscribe | ngOnDestroy + Subject | takeUntilDestroyed() |
| DOM-Messung nach Render | ngAfterViewInit + Timeout | afterNextRender({ read: ... }) |
| DOM bei jedem Render | ngAfterViewChecked | afterRender({ ... }) |
| Custom-Change-Detection | ngDoCheck | effect() auf das relevante Signal |
Häufige Fehler
Inputs im Constructor lesen
Bei klassischen @Input()-Decoratoren sind die Werte im Constructor noch undefined — Angular setzt sie erst kurz vor ngOnChanges. Wer dort this.userId liest, bekommt zuverlässig undefined. Lösung: in ngOnInit lesen — oder gleich auf die Signal-Variante input() umstellen, die ab Constructor-Zeit verfügbar ist.
ngOnChanges feuert nicht bei Mutationen
ngOnChanges reagiert nur auf Referenz-Wechsel der Input-Property. Wenn der Parent ein Array oder Objekt in-place mutiert (arr.push(x)), bleibt der Hook stumm — der Child sieht den neuen Wert zwar, aber bekommt keinen Trigger. Saubere Lösung: immer neue Referenzen senden (arr = […arr, x]) oder auf input()-Signals + effect() umstellen.
ngDoCheck als Performance-Killer
ngDoCheck wird bei JEDER Change-Detection aufgerufen — bei Default-Strategy mehrfach pro Frame, bei OnPush seltener. Teure Logik dort (Tiefenvergleich, JSON-Stringify, DOM-Reads) bremst die ganze App aus. Faustregel: für moderne Apps ist ngDoCheck praktisch nie nötig — nimm stattdessen ein effect() auf das Signal, das du beobachten willst.
ngOnDestroy läuft NICHT bei Tab-Close
Der Hook feuert nur, wenn die Komponente ordentlich aus dem DOM entfernt wird — nicht bei Browser-Tab-Close, Reload oder Hard-Crash. Wer „beim Verlassen warnen” oder „letzte Session-Daten senden” implementieren will, muss zusätzlich auf window.addEventListener(‘beforeunload’, …) oder visibilitychange hören und das Listener-Cleanup über DestroyRef regeln.
DestroyRef außerhalb des Injection-Contexts
inject(DestroyRef) funktioniert nur im Injection-Context: Constructor, Field-Initializer oder via runInInjectionContext(injector, …). Wer es in einer Methode aufruft, die durch einen User-Click ausgelöst wurde, bekommt NG0203. Lösung: DestroyRef einmal im Constructor injizieren und als Field speichern.
afterRender mit Signals — kein Tracking
Ein häufiges Missverständnis: afterRender(() => this.count()) wirkt wie ein Effect, ist aber keiner. Die Funktion läuft nach JEDEM Render — egal ob das gelesene Signal sich geändert hat. Für reaktive Side-Effects ist effect() richtig; afterRender ist ausschließlich für DOM-bezogene Operationen gedacht.
Lifecycle-Reihenfolge bei Parent + Child
Beim ersten Render läuft die Reihenfolge so: Parent-Constructor → Parent-ngOnInit → Child-Constructor → Child-ngOnInit → Child-ngAfterViewInit → Parent-ngAfterViewInit. Init feuert top-down, View-Init bottom-up. Wer im Parent-ngOnInit auf eine Child-@ViewChild-Methode zugreift, bekommt undefined — das geht erst ab Parent-ngAfterViewInit.
ExpressionChangedAfterItHasBeenCheckedError in *Checked-Hooks
Wer in ngAfterViewChecked oder ngAfterContentChecked ein gebundenes Property setzt, riskiert den berüchtigten ExpressionChangedAfterItHasBeenCheckedError im Dev-Mode. Grund: Angular hat den View bereits geprüft und sieht jetzt einen anderen Wert. Lösung: Schreib-Operationen entweder vor dem Check (in ngOnInit/ngAfterViewInit) erledigen oder asynchron via queueMicrotask/Signal verschieben.
Weiterführende Ressourcen
Externe Quellen
- Component Lifecycle – Angular.dev
- OnInit API Reference – Angular.dev
- OnDestroy API Reference – Angular.dev
- DestroyRef API Reference – Angular.dev
- afterRender API Reference – Angular.dev
- afterNextRender API Reference – Angular.dev