Risiken und Herausforderungen bei der Nutzung von Angular Signals
In komplexen Architekturen bringen Angular Signals eine Reihe Nachteile mit, die Systeme völlig aus dem Ruder laufen lassen können.
Mit Angular 17 hielten Signals 2023 offiziell Einzug in das Framework. Sie versprechen eine modernere, klarere Reaktivität: weniger Boilerplate-Code, bessere Performance. Gerade im Template- und Komponentenbereich lösen sie viele Probleme eleganter als klassische Observable-basierte Ansätze.
Statt Subscriptions, pipe() und komplexen Streams genügen nun wenige Zeilen mit signal(), computed() und effect(). Der Code wirkt schlanker, intuitiver und näher am User Interface (UI). Die Idee liegt nahe: Wenn Signals im UI überzeugen, warum nicht auch in der Applikationslogik? Ein Application Store ohne Actions, Meta-Framework und Observable: direkt, deklarativ, minimalistisch. Dieser Ansatz wird im Folgenden anhand eines konkreten Fallbeispiels analysiert und kritisch hinterfragt.
Aufbau des Fallbeispiels
Auf den ersten Blick besitzt dieses Beispiel einen klar strukturierten Architekturansatz. Das UI reagiert flüssig und der Code bleibt übersichtlich. Komplexe Streams und eigene Subscription Handling entfallen. Stattdessen kommen Signals zum Einsatz. Ein ProductStore übernimmt die Zustandslogik. Signals organisieren Kategorien, Filter und Produktdaten – reaktiv und direkt.
Die Struktur überzeugt zunächst durch Klarheit. Die Komponente konsumiert productList direkt, ohne eigene Logik. Der Store verwaltet den Zustand, Signals sorgen für die Weitergabe von Änderungen. Doch mit der nächsten Anforderung ändert sich das Bild: Bestimmte Produkte sollen im Katalog verbleiben, jedoch im UI nicht mehr erscheinen. Eine Anpassung der bestehenden API ist nicht möglich, und das Backend liefert eine Liste freigegebener Produkt-IDs, anhand derer das UI filtert.
@Injectable({ providedIn: 'root' })
export class ProductStore {
private allProducts = signal([]);
readonly selectedCategory = signal('Bücher');
readonly onlyAvailable = signal(false);
// Computed product list filtering based on availability and backend enabled product IDs
readonly productList = computed(() => {
return this.allProducts().filter(p =>
this.onlyAvailable() ? p.available : true
).filter(p => this.backendEnabledProductIds().has(p.id));
});
readonly backendEnabledProductIds = signal>(new Set());
constructor(private api: ProductApiService) {
effect(() => {
const category = this.selectedCategory();
const onlyAvailable = this.onlyAvailable();
this.api.getProducts(category, onlyAvailable).then(products => {
this.allProducts.set(products);
});
});
effect(() => {
this.api.getEnabledProductIds().then(ids => {
this.backendEnabledProductIds.set(new Set(ids));
});
});
}
} Obwohl die Architektur zunächst stabil bleibt, nimmt ihre Komplexität zu. Die Reaktivität funktioniert, jedoch wächst die Zahl der effect()s, und die Übersichtlichkeit leidet. Der Code, der anfangs klar strukturiert war, wird zunehmend undurchsichtig. Der ursprüngliche Fokus auf Einfachheit geht verloren, wenn Logik in verstreute effect()s übergeht. Diese Entwicklungen erfordern eine aufmerksame Betrachtung der Architektur.
Wenn reaktive Systeme entgleisen
Das Setup wirkt zunächst unspektakulär. Die Produktliste wird mithilfe eines computed() erstellt, gefiltert nach Verfügbarkeit und den vom Backend freigegebenen IDs. Doch der nächste Feature-Wunsch stellt das System auf die Probe. Eine Änderung der Kategorie löst ein Tracking-Event aus, und die Entwickler entscheiden sich, diese Funktionalität über ein effect() zu integrieren.
effect(() => {
const category = this.selectedCategory();
this.analytics.trackCategoryView(category);
});Obwohl dies automatisiert und ohne erkennbaren Einfluss erscheint, führt es zur Herausforderung, dass Signals nicht nur auf semantische Änderungen reagieren, sondern auf jegliche Mutation. Dies kann zu verzerrten Metriken resultieren, da doppelte Events erzeugt werden können. Die Reaktionskette wird unvorhersehbar, und das UI kann inkonsistent werden. Daten werden mehrfach geladen, und Seiteneffekte sind nicht mehr eindeutig zuzuordnen.
Der Kipppunkt
Die Unsichtbarkeit der Abhängigkeiten führt zu vermehrten effect()s, die schwer zu kontrollieren sind. Ein effect() kann bei jeder Änderung ausgelöst werden, auch wenn sich der Wert nicht geändert hat. In einem realen Projekt führte dies zu unnötig häufigen API-Anfragen, die das Backend überlasteten.
Das Missverständnis
effect() fungiert nicht wie ein klar definierter Lifecycle Hook, sondern als ein reaktiver Spion, der jede Änderung registriert, unabhängig von ihrer Bedeutung. Diese mangelnde Koordination führt dazu, dass Effekte isoliert wirken und schwer nachvollziehbar sind.
RxJS im Vergleich
Im Gegensatz dazu schafft RxJS einen klaren und kontrollierten Fluss von Reaktivität. Entwickelnde können explizit bestimmen, wann und wie Reaktionen stattfinden, beispielsweise durch Operatoren wie distinctUntilChanged() oder switchMap, um unerwünschte parallele Anfragen zu vermeiden.
this.selectedCategory$.pipe(
distinctUntilChanged(),
switchMap(category => this.api.loadStatsForCategory(category))
).subscribe(stats => {
this.categoryStats$.next(stats);
});Die Transparenz von RxJS unterscheidet sich erheblich von der in Signals, die oft die Kontrolle über die Reaktivität behindern und zu komplexen, intransparenten Interaktionen führen können.
Signals: Punktueller Schutz, aber keine Architektur
Signals bieten Möglichkeiten zur Sicherstellung der Reaktivität, jedoch erfordert deren Einsatz Disziplin und klare Konventionen. Ohne diese wird die Komplexität schnell überhandnehmen, vor allem in größeren Projekten, in denen mehrere Entwickler zusammenarbeiten.
Sinnvolle Einsatzbereiche für Signals
Signals sind am effektivsten, wenn der Zustand lokal und die Reaktivität überschaubar bleibt. Sie eignen sich besonders für UI-nahe Zustände, ableitbare Daten und Komponenten, die sich selbst aktualisieren. In Bereichen, wo Steuerung und Koordination notwendig sind, hingegen sollten strukturierte Architekturen wie RxJS den Vorzug erhalten.
Zusammengefasst ist der Erfolg beim Einsatz von Angular Signals stark von einer durchdachten Architektur und klaren Zuständigkeiten abhängig. Ein überlegter Umgang mit der Reaktivität kann Komplexität sichtbar und steuerbar machen, während ungeplanter Einsatz von effect()s zu unvorhersehbaren und schwer wartbaren Systemen führen kann.


