Die Lifecycle-Hooks (ngOnInit, ngOnChanges, ngOnDestroy ...) gibt es weiterhin, und sie funktionieren. Aber mit Signals werden die meisten überflüssig: Das Ableiten aus Inputs übernimmt computed(), Reaktionen effect(), die Arbeit mit dem DOM afterNextRender() und das Aufräumen DestroyRef.
Die Äquivalenztabelle
Alter Hook → Modernes Äquivalent
───────────────────────────────────────────────────────────────────
constructor → constructor (DI, signals, effect, afterNextRender)
ngOnChanges → computed() / effect() / linkedSignal() auf Inputs
ngOnInit → computed() / resource() (oder ngOnInit, weiterhin gültig)
ngDoCheck → brauchst du fast nie
ngAfterContentInit → contentChild() / contentChildren() (Signals)
ngAfterViewInit → afterNextRender() + viewChild()
ngAfterViewChecked → afterEveryRender() (selten)
ngOnDestroy → inject(DestroyRef).onDestroy() / takeUntilDestroyed()Wie eine moderne Komponente aussieht
import { Component, DestroyRef, afterNextRender, computed, effect, inject, input } from '@angular/core';
@Component({ selector: 'app-user-panel', /* ... */ })
export class UserPanel {
readonly userId = input.required<number>();
private readonly api = inject(UserApi); // DI: in Feldern / Konstruktor
// „ngOnChanges“ → computed / effect / resource auf Inputs
readonly user = httpResource<User>(() => `/api/users/${this.userId()}`);
readonly title = computed(() => this.user.value()?.name ?? 'Lädt …');
constructor() {
// „ngAfterViewInit“ → afterNextRender (einmal, nach dem ersten Rendering)
afterNextRender(() => this.chart.init(this.canvas().nativeElement));
// „ngOnDestroy“ → DestroyRef.onDestroy (funktioniert auch in Services, Funktionen, Direktiven)
inject(DestroyRef).onDestroy(() => this.chart.dispose());
// Reaktion auf die Änderung eines Inputs
effect(() => this.analytics.track('user-panel-open', { id: this.userId() }));
}
}- Klicke auf „Child erzeugen“.
ngOnChanges mit typisiertem SimpleChanges v21
// Brauchst du noch ngOnChanges? Ab v21 ist SimpleChanges generisch:
export class Legacy implements OnChanges {
readonly name = input('');
readonly age = input(0);
ngOnChanges(changes: SimpleChanges<Legacy>) {
if (changes.name) { // typisiert, ohne changes['name']
console.log(changes.name.previousValue, '→', changes.name.currentValue);
}
}
}DestroyRef: wiederverwendbares Aufräumen
Anders als ngOnDestroy (das es nur in Klassen gibt) lässt sich DestroyRef überall injizieren — auch in Funktionen. Das ermöglicht wiederverwendbare „Hooks“, die von selbst aufräumen.
// DestroyRef funktioniert überall, wo ein Injection-Kontext existiert — ideal für wiederverwendbare Funktionen
export function useInterval(ms: number, fn: () => void): void {
const id = setInterval(fn, ms);
inject(DestroyRef).onDestroy(() => clearInterval(id));
}
export class Clock {
readonly now = signal(new Date());
constructor() {
useInterval(1000, () => this.now.set(new Date())); // räumt von selbst auf
}
}afterNextRender / afterEveryRender
// afterNextRender — einmal; afterEveryRender — nach jedem Rendering der Anwendung (selten nötig)
afterNextRender({
read: () => this.el.nativeElement.offsetHeight, // liest das Layout
// write: (height) => ... // schreibt dann (getrennte Phasen = kein Thrashing)
});
// Läuft nur im Browser (nicht bei SSR): der richtige Ort für window, document, localStorage#1 Ist der Build durch?
#2 Ja, und alle Tests sind grün.
#3 Dann mache ich den PR auf.
#4 Sag Bescheid, wenn er gemergt ist.
send() {
this.messages.update((list) => [...list, next]);
const el = this.chat().nativeElement;
el.scrollTop = el.scrollHeight; // das DOM hat noch die alte Liste
}Das Aufräumen von Subscriptions, Timern und Listenern beschreibt Memory Leaks finden und beheben im Detail; Code, der window oder document berührt, muss auch auf dem Server laufen: Servertauglicher Code.
Wann ja, wann nein
- Bestehender Code, der funktioniert — ein Rewrite muss nicht erzwungen werden.
ngOnInitfür eine Initialisierung, die Inputs einmal liest (ist weiterhin gültig und gut lesbar).- Von Bibliotheken geforderte Interfaces (z. B. der klassische
ControlValueAccessor).
- Einen Wert neu berechnen, wenn sich ein Input ändert →
computed(). - Lokalen Zustand zurücksetzen, wenn sich ein Input ändert →
linkedSignal(). - Daten laden, wenn sich ein Input ändert →
httpResource(). - Abmelden in
ngOnDestroy→takeUntilDestroyed()/toSignal.
Häufige Stolperfallen
- Inputs im Konstruktor: Sie sind noch nicht gesetzt. Das Lesen eines
input.requiredwirft dort einen Fehler. window/documentim Konstruktor bei SSR → ein Fehler auf dem Server. Verschiebe den Code inafterNextRender.ngOnChangeswird nicht ausgelöst bei internen Mutationen von Objekten, die du erhältst — nur bei neuen Referenzen.