13 Lifecycle Hooks: Kontrolle über den Component-Lebenszyklus

13.1 Das Component-Lifecycle-Problem

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.

13.2 Constructor: Nicht ein Lifecycle Hook

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.

13.3 ngOnChanges: Input-Property Tracking

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.

13.4 ngOnInit: Der primäre Initialization Hook

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.

13.5 ngDoCheck: Custom Change Detection

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.

13.6 ngAfterContentInit: Content Projection verfügbar

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.

13.7 ngAfterContentChecked: Nach Content Change Detection

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.

13.8 ngAfterViewInit: View ist fertig rendered

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;
  });
}

13.9 ngAfterViewChecked: Nach View Change Detection

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.

13.10 ngOnDestroy: Cleanup und Resource-Freeing

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) {}
}

13.11 Signals und Effects: Die neue Reactivity

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.

13.12 Lifecycle-Timing und Reihenfolge

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.

13.13 Common Patterns und Anti-Patterns

13.13.1 Pattern: Lazy-Loading mit ngOnInit

@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.

13.13.2 Anti-Pattern: Heavy Logic in ngDoCheck

// 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()));

13.13.3 Anti-Pattern: Subscription ohne Cleanup

// 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.