{"id":312815,"date":"2025-08-02T07:50:10","date_gmt":"2025-08-02T07:50:10","guid":{"rendered":"https:\/\/www.europesays.com\/de\/312815\/"},"modified":"2025-08-02T07:50:10","modified_gmt":"2025-08-02T07:50:10","slug":"angular-signals-elegante-reaktivitaet-als-architekturfalle","status":"publish","type":"post","link":"https:\/\/www.europesays.com\/de\/312815\/","title":{"rendered":"Angular Signals: Elegante Reaktivit\u00e4t als Architekturfalle"},"content":{"rendered":"<p>Mit Angular 17 hielten Signals 2023 offiziell Einzug in das Framework. Sie versprechen eine modernere, klarere Reaktivit\u00e4t: weniger Boilerplate-Code, bessere Performance. Gerade im Template- und Komponentenbereich l\u00f6sen sie viele Probleme eleganter als klassische Observable-basierte Ans\u00e4tze.<\/p>\n<p>    <img loading=\"lazy\" decoding=\"async\" alt=\"Nicolai Wolko\" height=\"754\" src=\"data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' width='696px' height='391px' viewBox='0 0 696 391'%3E%3Crect x='0' y='0' width='696' height='391' fill='%23f2f2f2'%3E%3C\/rect%3E%3C\/svg%3E\" style=\"aspect-ratio: 754 \/ 754; object-fit: cover;\" width=\"754\"\/><\/p>\n<p class=\"a-inline-textbox__synopsis\">\n          Nicolai Wolko ist Softwarearchitekt, Consultant und Mitgr\u00fcnder der WBK Consulting AG. Er unterst\u00fctzt Unternehmen bei komplexen Web- und Cloudprojekten und wirkt als Sparringspartner sowie Gutachter f\u00fcr CTOs. Fachbeitr\u00e4ge zur modernen Softwarearchitektur ver\u00f6ffentlicht er regelm\u00e4\u00dfig in Fachmedien und <a href=\"http:\/\/nicolaiwolko.ch\/blog\" rel=\"noopener noreferrer\" target=\"_blank\">auf seinem Blog<\/a>.&#13;<br \/>\n&#13;<\/p>\n<p>Statt Subscriptions, pipe() und komplexen Streams gen\u00fcgen nun wenige Zeilen mit signal(), computed() und effect(). Der Code wirkt schlanker, intuitiver und n\u00e4her am User Interface (UI).<\/p>\n<p>Da liegt die Idee nahe: Wenn Signals im UI \u00fcberzeugen, warum nicht auch in der Applikationslogik? Warum nicht RxJS vollst\u00e4ndig ersetzen? Ein Application Store ohne Actions, Meta-Framework und Observable: direkt, deklarativ, minimalistisch.<\/p>\n<p>Ein Ansatz, der im Folgenden anhand eines konkreten Fallbeispiels analysiert und kritisch hinterfragt wird. Anschlie\u00dfend wird behandelt, in welchen Kontexten sich Signals sinnvoll einsetzen lassen.<\/p>\n<p>Aufbau des Fallbeispiels<\/p>\n<p>Auf den ersten Blick besitzt dieses Beispiel einen klar strukturierten Architekturansatz. Doch der Wandel beginnt unauff\u00e4llig. RxJS bleibt zun\u00e4chst au\u00dfen vor. Das UI reagiert fl\u00fcssig, der Code bleibt \u00fcbersichtlich. Komplexe Streams, verschachtelte Operatoren oder eigenes Subscription Handling entfallen. Stattdessen kommen Signals zum Einsatz. Es liegt nahe, diese unkomplizierte Herangehensweise auch f\u00fcr die Applikationslogik zu \u00fcbernehmen. Im folgenden Beispiel \u00fcbernimmt ein ProductStore die Zustandslogik. Signals organisieren Kategorien, Filter und Produktdaten \u2013 reaktiv und direkt.<\/p>\n<p>@Injectable({ providedIn: &#8218;root&#8216; })<br \/>\nexport class ProductStore {<br \/>\n  private allProducts = signal([]);<br \/>\n  readonly selectedCategory = signal(&#8218;B\u00fccher&#8216;);<br \/>\n  readonly onlyAvailable = signal(false);<\/p>\n<p>  readonly productList = computed(() =&gt; {<br \/>\n    return this.allProducts().filter(p =&gt;<br \/>\n      this.onlyAvailable() ? p.available : true<br \/>\n    );<br \/>\n  });<\/p>\n<p>  selectCategory(category: string) {<br \/>\n    this.selectedCategory.set(category);<br \/>\n  }<\/p>\n<p>  toggleAvailabilityFilter() {<br \/>\n    this.onlyAvailable.set(!this.onlyAvailable());<br \/>\n  }<\/p>\n<p>  constructor(private api: ProductApiService) {<br \/>\n    effect(() =&gt; {<br \/>\n      const category = this.selectedCategory();<br \/>\n      const onlyAvailable = this.onlyAvailable();<br \/>\n      this.api.getProducts(category, onlyAvailable).then(products =&gt; {<br \/>\n        this.allProducts.set(products);<br \/>\n      });<br \/>\n    });<br \/>\n  }<br \/>\n}<\/p>\n<p>Die Struktur \u00fcberzeugt zun\u00e4chst durch Klarheit. Die Komponente konsumiert productList direkt, ohne eigene Logik. Der Store verwaltet den Zustand, Signals sorgen f\u00fcr die Weitergabe von \u00c4nderungen.<\/p>\n<p>Doch mit der n\u00e4chsten Anforderung \u00e4ndert sich das Bild: Bestimmte Produkte sollen zwar im Katalog verbleiben, aber im UI nicht mehr erscheinen. Da auch andere Systeme die bestehende API verwenden, ist eine Anpassung nicht m\u00f6glich. Stattdessen liefert das Backend eine Liste freigegebener Produkt-IDs, anhand derer das UI filtert.<\/p>\n<p>@Injectable({ providedIn: &#8218;root&#8216; })<br \/>\nexport class ProductStore {<br \/>\n  \/\/ [&#8230;]<\/p>\n<p>  readonly backendEnabledProductIds = signal&gt;(new Set());<\/p>\n<p>  readonly productList = computed(() =&gt; {<br \/>\n    return this.allProducts().filter(p =&gt;<br \/>\n      this.onlyAvailable() ? p.available : true<br \/>\n    ).filter(p =&gt; this.backendEnabledProductIds().has(p.id));<br \/>\n  });<\/p>\n<p>  constructor(private api: ProductApiService) {<br \/>\n    effect(() =&gt; {<br \/>\n      const category = this.selectedCategory();<br \/>\n      const onlyAvailable = this.onlyAvailable();<br \/>\n      this.api.getProducts(category, onlyAvailable).then(products =&gt; {<br \/>\n        this.allProducts.set(products);<br \/>\n      });<br \/>\n    });<\/p>\n<p>    effect(() =&gt; {<br \/>\n      this.api.getEnabledProductIds().then(ids =&gt; {<br \/>\n        this.backendEnabledProductIds.set(new Set(ids));<br \/>\n      });<br \/>\n    });<br \/>\n  }<\/p>\n<p>  \/\/ [&#8230;]<br \/>\n}<\/p>\n<p>Nach au\u00dfen bleibt die Architektur zun\u00e4chst unver\u00e4ndert. Die Komponente enth\u00e4lt weiterhin keine eigene Logik, Subscriptions sind nicht notwendig, und die Reaktivit\u00e4t scheint erhalten zu bleiben. Im Service jedoch nimmt die Zahl der effect()s zu, Abh\u00e4ngigkeiten werden vielf\u00e4ltiger, und die \u00dcbersichtlichkeit leidet.<\/p>\n<p>Nach und nach wandert Logik in verteilte effect()s, bis ihre Zust\u00e4ndigkeiten kaum noch greifbar sind. Aus einem \u00fcberschaubaren ViewModel entsteht ein Gebilde mit immer mehr impliziten Reaktionen \u2013 eine Entwicklung, die ein waches Auge f\u00fcr Architektur erfordert.<\/p>\n<p>Wenn reaktive Systeme entgleisen<\/p>\n<p>Das Setup wirkt zun\u00e4chst unspektakul\u00e4r. Die Produktliste wird \u00fcber ein computed() erstellt, gefiltert nach Verf\u00fcgbarkeit und den vom Backend freigegebenen IDs. Zwei effect()s laden die Daten.<\/p>\n<p>Der Code wirkt aufger\u00e4umt und l\u00e4sst sich modular erweitern. Doch der n\u00e4chste Feature-Wunsch stellt das System auf die Probe: Die Stakeholder m\u00f6chten wissen, wie oft bestimmte Kategorien angesehen werden. Die Entwicklerinnen und Entwickler entscheiden sich f\u00fcr einen naheliegenden Ansatz. Eine \u00c4nderung der Kategorie l\u00f6st ein Tracking-Event aus. Ein effect() scheint daf\u00fcr perfekt geeignet \u2013 unkompliziert und ohne erkennbare Nebenwirkungen:<\/p>\n<p>effect(() =&gt; {<br \/>\n  const category = this.selectedCategory();<br \/>\n  this.analytics.trackCategoryView(category);<br \/>\n});<\/p>\n<p>Schnell eingebaut, kein zus\u00e4tzlicher State, keine neue Subscription. Eine Reaktion auf das bestehende Signal, unkompliziert und ohne erkennbare Nebenwirkungen. Doch damit verl\u00e4sst der Code den Bereich kontrollierter Reaktivit\u00e4t.<\/p>\n<p>Der Kipppunkt<\/p>\n<p>Die Annahme ist klar: \u00c4ndert sich die Kategorie, wird ein Tracking ausgel\u00f6st. Was dabei leicht zu \u00fcbersehen ist: Signals reagieren nicht auf Bedeutung, sondern auf jede Mutation. Auch wenn set() denselben Wert schreibt oder zwei Komponenten nacheinander dieselbe Auswahl treffen, passiert zwar technisch etwas, semantisch aber nicht. Das Ergebnis sind doppelte Events und verzerrte Metriken, ohne dass der Code einen Hinweis darauf gibt. Alles sieht korrekt aus.<\/p>\n<p>Das Tracking erfolgt unmittelbar im selben Ausf\u00fchrungstakt (Tick), ohne M\u00f6glichkeit zur Entkopplung. Wenn parallel ein weiterer effect() ausgel\u00f6st wird \u2013 etwa durch ein zweites Signal \u2013, fehlt jegliche Koordination.<\/p>\n<p>Die Reihenfolge ist nicht vorhersehbar, und das UI kann in einen inkonsistenten Zustand geraten: Daten werden mehrfach geladen, Reaktionen \u00fcberschneiden sich, Seiteneffekte sind nicht mehr eindeutig zuzuordnen. Mit jedem zus\u00e4tzlichen effect() steigt die Zahl impliziter Wechselwirkungen. Was wie ein reagierendes System wirkt, ist l\u00e4ngst nicht mehr entscheidungsf\u00e4hig.<\/p>\n<p>In einem Kundenprojekt f\u00fchrte genau dieser Zustand dazu, dass ein effect() mehrfach pro Sekunde ausl\u00f6ste. Nicht wegen einer echten \u00c4nderung, sondern weil derselbe Wert mehrfach gesetzt wurde. Das UI zeigte korrekte Daten, aber das Backend war mit redundanten Anfragen \u00fcberlastet.<\/p>\n<p>Das Missverst\u00e4ndnis<\/p>\n<p>effect() wirkt wie ein deklarativer Controller: &#8222;Wenn sich X \u00e4ndert, tue Y.&#8220; Doch in Wirklichkeit ist es ein reaktiver Spion. Er beobachtet jedes Signal, das gelesen wird, unabh\u00e4ngig von der semantischen Bedeutung. Er feuert sogar dann, wenn niemand es erwartet. Und er ist nicht koordiniert. Jeder effect() lebt in seiner eigenen Welt, ohne zentrale Regie.<\/p>\n<p>Was als architektonische Vereinfachung begann, endet in einer Blackbox aus Zust\u00e4nden, Reaktionen und Nebenwirkungen. Mit jedem weiteren Feature w\u00e4chst diese Komplexit\u00e4t. Es gibt keinen gro\u00dfen Knall, aber eine zuvor elegant erscheinende Struktur driftet leise auseinander.<\/p>\n<p>\n      Dieser Link ist leider nicht mehr g\u00fcltig.\n    <\/p>\n<p>Links zu verschenkten Artikeln werden ung\u00fcltig,<br \/>\n      wenn diese \u00e4lter als 7\u00a0Tage sind oder zu oft aufgerufen wurden.\n    <\/p>\n<p><strong>Sie ben\u00f6tigen ein heise+ Paket, um diesen Artikel zu lesen. Jetzt eine Woche unverbindlich testen \u2013 ohne Verpflichtung!<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"Mit Angular 17 hielten Signals 2023 offiziell Einzug in das Framework. Sie versprechen eine modernere, klarere Reaktivit\u00e4t: weniger&hellip;\n","protected":false},"author":2,"featured_media":312816,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[135],"tags":[89782,29,47841,30,196,89783,190,189,89784,194,191,41528,193,192],"class_list":{"0":"post-312815","1":"post","2":"type-post","3":"status-publish","4":"format-standard","5":"has-post-thumbnail","7":"category-wissenschaft-technik","8":"tag-angular","9":"tag-deutschland","10":"tag-developer","11":"tag-germany","12":"tag-it","13":"tag-javascript-framework","14":"tag-science","15":"tag-science-technology","16":"tag-softwarearchitektur","17":"tag-technik","18":"tag-technology","19":"tag-webentwicklung","20":"tag-wissenschaft","21":"tag-wissenschaft-technik"},"share_on_mastodon":{"url":"https:\/\/pubeurope.com\/@de\/114958082132754692","error":""},"_links":{"self":[{"href":"https:\/\/www.europesays.com\/de\/wp-json\/wp\/v2\/posts\/312815","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.europesays.com\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.europesays.com\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.europesays.com\/de\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.europesays.com\/de\/wp-json\/wp\/v2\/comments?post=312815"}],"version-history":[{"count":0,"href":"https:\/\/www.europesays.com\/de\/wp-json\/wp\/v2\/posts\/312815\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.europesays.com\/de\/wp-json\/wp\/v2\/media\/312816"}],"wp:attachment":[{"href":"https:\/\/www.europesays.com\/de\/wp-json\/wp\/v2\/media?parent=312815"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.europesays.com\/de\/wp-json\/wp\/v2\/categories?post=312815"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.europesays.com\/de\/wp-json\/wp\/v2\/tags?post=312815"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}