effect() führt eine Funktion einmal aus und danach jedes Mal erneut, wenn sich ein gelesenes Signal ändert. Er ist die Brücke zwischen der reaktiven Welt (Signals) und der Welt, die nichts von Signals weiß: manuelles DOM, localStorage, Drittanbieter-Bibliotheken, Analytics, die Konsole.
Ein Effect bringt Daten aus Signals nach außen. Wenn du damit ein anderes Signal setzt, brauchtest du fast sicher ein computed() oder ein linkedSignal().
Syntax
Du erstellst den Effect in einem Injection-Kontext (Konstruktor oder Feld-Initialisierung). Der Effect ist automatisch an den Lebenszyklus der Komponente oder des Service gebunden: Wird diese zerstört, wird auch der Effect beendet.
import { Component, effect, signal } from '@angular/core';
@Component({ /* ... */ })
export class Settings {
readonly theme = signal<'light' | 'dark'>('light');
constructor() {
// Läuft einmal bei der Erstellung, danach jedes Mal, wenn sich `theme` ändert.
effect(() => {
document.documentElement.dataset['theme'] = this.theme();
localStorage.setItem('theme', this.theme());
});
}
}Wann genau läuft er?
- Nicht synchron: Wenn du ein Signal dreimal hintereinander setzt, läuft der Effect nur einmal, mit dem finalen Wert.
- Komponenten-Effects laufen während der Change Detection, bevor das Template der Komponente geprüft wird.
- Effects in Root-Services laufen als Microtasks, vor der Change Detection.
- Die Abhängigkeiten sind dynamisch: Nur die im letzten Durchlauf gelesenen Signals zählen.
Cleanup: onCleanup
Der Effect erhält eine onCleanup-Funktion. Der registrierte Callback läuft vor der nächsten Ausführung und bei der Zerstörung. Hier stoppst du Timer, entfernst Listener und brichst Requests ab.
effect((onCleanup) => {
if (!this.running()) return;
const id = setInterval(() => this.ticks.update((t) => t + 1), this.speed());
// Wird vor der nächsten Ausführung des Effects aufgerufen
// und wenn die Komponente / der Service zerstört wird.
onCleanup(() => clearInterval(id));
});- Drücke „Start“. Ändere das Intervall, während er läuft.
afterNextRender & afterRenderEffect: wenn du das DOM brauchst
Wenn du das DOM messen oder ändern musst, nachdem Angular es aktualisiert hat, nutze die Render-Hooks. afterNextRender läuft nur einmal (perfekt, um eine Bibliothek für Diagramme oder Karten zu initialisieren), und afterRenderEffect ist ein Effect, der nach dem Rendern läuft, aber nur, wenn sich seine Signals geändert haben. Beide laufen nur im Browser (nicht auf dem Server beim SSR).
import { afterNextRender, afterRenderEffect, ElementRef, viewChild } from '@angular/core';
export class AutoGrowTextarea {
private readonly area = viewChild.required<ElementRef<HTMLTextAreaElement>>('area');
readonly text = signal('');
constructor() {
// Läuft NACHDEM Angular das DOM aktualisiert hat, sobald sich `text` ändert.
afterRenderEffect(() => {
this.text();
const el = this.area().nativeElement;
el.style.height = 'auto';
el.style.height = `${el.scrollHeight}px`;
});
// Einmalig, nach dem ersten Rendern (ideal für Bibliotheken, die das DOM brauchen).
afterNextRender(() => {
this.chart = new ThirdPartyChart(this.canvas().nativeElement);
});
}
}
// Mit expliziten Phasen — vermeidet „Layout Thrashing“ bei vielen Komponenten:
afterRenderEffect({
earlyRead: () => this.host.nativeElement.getBoundingClientRect().width, // liest das DOM
write: (width) => { this.panel().nativeElement.style.width = `${width()}px`; }, // schreibt das DOM
});Anti-Patterns (und was du stattdessen nutzt)
1. Synchronisieren zwischen Signals
// ✓ computed für abgeleiteten Zustand
readonly fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
// ✓ linkedSignal, wenn der abgeleitete Wert auch editierbar sein muss
readonly selected = linkedSignal(() => this.options()[0]);// ✗ effect, der Zustand „synchronisieren“ soll
readonly firstName = signal('Ana');
readonly lastName = signal('Pop');
readonly fullName = signal('');
constructor() {
effect(() => this.fullName.set(`${this.firstName()} ${this.lastName()}`));
}
// Probleme: ein zusätzlicher Durchlauf, ein Moment, in dem fullName veraltet ist,
// und ein Abhängigkeitsgraph, der schwer nachzuvollziehen ist.firstName.set(…)computed()Ana Popeffect() + set()Ana PopEffect-Läufe: 1Auf dem Bildschirm sehen beide gleich aus: Der Effect einer Komponente läuft bei der Change Detection, bevor ihr Template aktualisiert wird. Der Unterschied zeigt sich in Code, der den Wert direkt nach set() liest.
Im selben Klick-Handler gelesen, direkt nach set()
- Klicke auf einen Namen: Du siehst, was jede Variante genau in diesem Moment liefert.
2. Daten aus einem Effect laden
// ✓ resource / httpResource: automatischer Abbruch, Status, Fehler, Reload
readonly user = httpResource<User>(() => `/api/users/${this.userId()}`);// ✗ HTTP aus einem effect ausgelöst
effect(() => {
const id = this.userId();
this.http.get<User>(`/api/users/${id}`).subscribe((u) => this.user.set(u));
// kein Abbruch: alte Responses können nach den neuen ankommen (Race Condition)
});Der Injection-Kontext
effect() braucht einen Injector, um zu wissen, wann er sich zerstören soll. Außerhalb des Konstruktors bekommst du den Fehler NG0203. Die Lösung: Erstelle den Effect als Feld oder im Konstruktor; in seltenen Fällen übergibst du den injector explizit.
export class Dashboard {
private readonly injector = inject(Injector);
// ✓ in einer Feld-Initialisierung oder im Konstruktor: Der Injection-Kontext ist verfügbar
private readonly logEffect = effect(() => console.log(this.filter()));
ngOnInit() {
// ✗ hier bist du nicht mehr in einem Injection-Kontext → Fehler NG0203
// effect(() => ...);
// ✓ wenn du es wirklich später brauchst, übergib den Injector explizit
effect(() => console.log(this.filter()), { injector: this.injector });
}
stop() {
this.logEffect.destroy(); // manuell stoppen (selten nötig — er zerstört sich mit der Komponente selbst)
}
}Bevor du einen Effect schreibst, prüfe, ob es nicht abgeleiteter State (signal() und computed()) oder zurücksetzbarer State (linkedSignal() — editierbarer abgeleiteter Zustand) ist. Warum ein Effect genau dann läuft, erklärt Signals unter der Haube.
Wann ja, wann nein
- Persistenz:
localStorage,sessionStorage, IndexedDB. - Logging, Analytics, Telemetrie.
- Imperative Bibliotheken: Diagramme (Chart.js), Karten (Leaflet), Editoren, Canvas.
- Browser-APIs:
document.title, Fokus, Scrollen, Klassen auf<html>. - Synchronisation mit Nicht-Angular-Code (ein alter Store, ein jQuery-Widget).
- Abgeleiteter Zustand →
computed(). - Editierbarer / zurücksetzbarer abgeleiteter Zustand →
linkedSignal(). - Requests bei Änderung eines Parameters →
resource()/httpResource(). - Reaktion auf einen Klick → die Logik direkt in den Event-Handler schreiben.
- Formularvalidierung → Signal Forms haben ihre eigenen Regeln.
Häufige Stolperfallen
- Endlosschleifen: Ein Effect, der dasselbe Signal liest und schreibt, kann sich endlos neu auslösen. Nutze
untracked(), um den aktuellen Wert zu lesen, oder überdenke besser den Ablauf. - Versteckte Abhängigkeiten: Wenn du eine Methode aufrufst, die intern Signals liest, werden auch diese zu Abhängigkeiten. Packe den Aufruf in
untracked(() => ...). - Synchrone Ausführung erwarten: Nach
signal.set()ist der Effect noch NICHT gelaufen. Schreibe keinen Code, der davon abhängt. - effect in ngOnInit: ein Injection-Kontext-Fehler. Verschiebe ihn in den Konstruktor.
Spickzettel
effect(() => { /* liest Signals, führt Seiteneffekte aus */ });
effect((onCleanup) => { const id = setInterval(...); onCleanup(() => clearInterval(id)); });
effect(fn, { injector, manualCleanup: true, debugName: 'sync-storage' });
const ref = effect(fn); ref.destroy();
afterNextRender(() => { /* einmal, nach dem ersten Rendern — DOM-Bibliothek initialisieren */ });
afterRenderEffect(() => { /* nach jedem Rendern, bei dem sich die gelesenen Signals geändert haben */ });
afterRenderEffect({ earlyRead, write, mixedReadWrite, read });
// Regel: effect = „reaktive Welt → nicht-reaktive Welt“. Niemals Signal → Signal.