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.

TypeScript hooks-overview.component.ts
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

HookZeitpunktTypischer Use-Case
ngOnChangesVor ngOnInit und bei jeder Input-ÄnderungAuf neue Input-Werte reagieren (klassisch)
ngOnInitEinmalig nach dem ersten ngOnChangesInitiale Daten laden, Felder ableiten
ngDoCheckBei JEDER Change-DetectionCustom-Change-Detection (selten nötig)
ngAfterContentInitEinmalig nach erstem Content-Projection-Check@ContentChild-Resultate lesen
ngAfterContentCheckedNach jedem Content-CheckAuf projizierten Inhalt reagieren
ngAfterViewInitEinmalig nach erstem View-Check@ViewChild-Resultate, DOM-Init
ngAfterViewCheckedNach jedem View-CheckView-Verifizierung (selten)
ngOnDestroyDirekt vor Component-ZerstörungCleanup: 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.

TypeScript user-detail.component.ts
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).

TypeScript filter-panel.component.ts
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.

TypeScript modal.component.ts
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.

TypeScript ticker.component.ts
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.

TypeScript destroy-ref.component.ts
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:

PhaseZweck
earlyReadDOM lesen, BEVOR geschrieben wird
writeDOM schreiben (Styles, Attribute)
mixedReadWriteDefault — nur als Fallback
readDOM lesen, NACHDEM alle Writes durch sind
TypeScript chart-mount.component.ts
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, ngAfterViewChecked laufen 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:

TypeScript tooltip.component.ts
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

AufgabeKlassischModern
Initial-Daten ladenngOnInitConstructor + Signal-Init / resource()
Auf Input-Änderung reagierenngOnChangesinput() + effect() / computed()
@ViewChild lesenngAfterViewInitviewChild() (Signal-Query)
Cleanup beim DestroyngOnDestroyinject(DestroyRef).onDestroy(...)
RxJS unsubscribengOnDestroy + SubjecttakeUntilDestroyed()
DOM-Messung nach RenderngAfterViewInit + TimeoutafterNextRender({ read: ... })
DOM bei jedem RenderngAfterViewCheckedafterRender({ ... })
Custom-Change-DetectionngDoCheckeffect() 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

/ Weiter

Zurück zu Komponenten

Zur Übersicht