
Architecting Angular Applications - Teil 2: Components
Veröffentlicht am 22. Juni 2025 - ⏱️ Ca. (wird berechnet) min. Lesezeit
Components sind die Achilles Ferse und Grundlage jeder Angular Architektur. Wer sie zu breit aufstellt, der erzeugt komplexen Wildwuchs. Nach kurzer Zeit hat man keine Chance mehr die Komplexität zu überblicken. Obwohl Components der häufigste und vermeintlich klarste Baustein im Angular Framework sind, sieht man in der Praxis eine Fülle von Problemen! Bereits hier entscheidet sich oft, ob eine Anwendung langfristig wartbar bleibt oder sich früh in technische Schulden verstrickt.
Wer UI-Components als Architekturbausteine versteht, schafft die Voraussetzung für eine skalierbare, robuste Anwendung. Die zentrale Frage ist: Wo endet UI-Logik, wo beginnt Business-Logik?
Der Artikel ist der zweite Teil der Serie und legt das Fundament für alle weiteren Architekturentscheidungen. Er zeigt typische Fallstricke in der Component-Gestaltung und liefert klare Prinzipien für eine robuste, testbare UI-Schicht.
Was Components ausmacht
Components sind die sichtbarste Schicht einer Angular-Anwendung. Ihre Verantwortung ist das UI-Verhalten und der UI Zustand. Geschäftslogik und domänenrelevanter State gehören in separate Schichten, nicht in die Component. Typische Fehler entstehen, wenn Components fachliche Entscheidungen treffen oder API-Interaktionen direkt steuern. Klare Verantwortlichkeiten halten die UI-Schicht schlank, verständlich und wartbar. Für mich persönlich hat sich folgende Regel als sehr wertvoll erwiesen: Hat eine Component mehr als 200 Zeilen Code? Dann geh sie nochmal durch und rechtfertige, warum sie so viel Code haben muss.
Dieses Prinzip folgt dem Single Responsibility Principle von Robert C. Martin (Uncle Bob): Eine UI-Component sollte genau eine Verantwortung haben: UI-Verhalten und Darstellung.
Architektur Prinzip: Eine UI-Component ist kein Trichter für Logik. Je weniger sie weiss, desto robuster bleibt die Architektur.
Was aber bedeutet das genau? UI Verhalten und Darstellung sind dehnbar, oder? Nein! Schauen wir uns typische Negativbeispiele an.
Anti-Pattern: Schlechte Praxis
Wie man es nicht machen sollte:
export class CheckoutComponent {
readonly cartItems = this.cartService.cartItemsSignal;
constructor(
private cartService: CartService,
private checkoutService: CheckoutService,
private http: HttpClient
) { }
submitOrder() {
const items = this.cartItems();
if (items.length === 0) {
alert('Cart is empty!');
return;
}
const discount = items.length > 10;
this.http.post('/api/orders', {
items,
discount,
}).subscribe(() => {
alert('Order placed!');
});
}
}
Warum ist dieses Beispiel problematisch?
Die Component übernimmt die Verantwortung für API-Calls, Business-Logik und Validierung gleichzeitig.
Sie kennt Details zur Cart-Größe und zur Rabattberechnung und verschickt direkt HTTP-Requests.
Das verletzt die Schichtentrennung und koppelt UI, Geschäftslogik und Infrastruktur unnötig eng zusammen.
Bei Änderungen an Rabattregeln oder der Checkout-API müsste die UI-Component angepasst werden. Ein klarer Architekturbruch.
Wie kann man es besser strukturieren? Das folgende Beispiel zeigt eine architekturkonforme Variante:
Beispiel 2: Architekturkonforme Component
export class CheckoutComponent {
constructor(private checkoutService: CheckoutService) {}
submitOrder() {
this.checkoutService.submitOrder();
}
}
Diese Component delegiert den Business-Prozess an einen Service und bleibt auf UI-Interaktion fokussiert.
Der CheckoutService
kapselt API-Logik und fachliche Prüfungen. Die
CheckoutComponent
kennt die Details der Cart-Logik nicht. Das ist ein absoluter klassiker.
Es wurden 2 komplett unterschiedliche Verantwortungen, Cart und Checkout, miteinander vermischt.
In diesem Fall ist Cart eine eigene Komponente. Denn: Checkout sollte nie über den Zustand des Carts verfügen
müssen.
Diesen Fehler sieht man in der Praxis tatsächlich mit am häufigsten. Er entsteht denkbar einfach: Ein neues Feature soll implementiert werden. Das ist doch schnell gemacht, hier schnell eine Funktion, funktioniert: Top! - Nein! Für einen Prototyp ist das vollkommen ok. Aber nach dem POC sollte dringend IMMER reflektiert werden: Jetzt wo es funktioniert: Wie müsste es richtig sein, damit es auch langfristig wartbar funktioniert. Wird dieser letzte, entscheidende Schritt weggelassen, entstehen genau hier die technischen Schulden!
Nur Logik, die direkt der Darstellung dient, gehört in die Component. Eine ideal strukturierte UI-Component bleibt klein und klar abgegrenzt.
Architektur Prinzip: Trifft eine Component Geschäftsentscheidungen, ist sie kein UI-Baustein mehr, sondern ein Architekturproblem.
Smart und Dumb Components
Warum ist die Trennung von Smart und Dumb Components ein Architekturthema? Ganz einfach: Sie hilft, die Schichtengrenzen in der UI bewusst und nachvollziehbar zu ziehen. Smart Components orchestrieren den Ablauf, holen Daten und koordinieren die Darstellung. Dumb Components dagegen sind rein darstellend und vollständig konfigurierbar von aussen. Ohne diese Trennung entstehen enge Kopplungen, die die Testbarkeit und Wiederverwendbarkeit massiv erschweren.
Diese Trennung ist ursprünglich als Konzept im React-Umfeld populär geworden. Besonders lesenswert ist der Originalartikel von Dan Abramov, der die Grundidee von Smart und Dumb Components beschreibt. Abramov selbst hat dieses Pattern später explizit relativiert und vor dogmatischer Anwendung gewarnt. In React ermöglichten Hooks später eine flexiblere Organisation komplexer Logik innerhalb einer Component. Angular bietet jedoch kein vergleichbares Hooks-Konzept. Hier bleibt die explizite Trennung von Orchestrierung und Darstellung weiterhin ein wertvolles Architekturprinzip und hochrelevant. Ich bin ein grosser Fan dieser Gliederung. Schon oft habe ich in der Praxis gesehen, wie sich Entwickler mit dem Management von Inputs und gleichzeitiger Orchestrierung schwergetan haben. Zurecht! Denn dann werden zwei verschiedene Verantwortungen vermischt. So entsteht Komplexität.
Wie sieht es konkret aus? Schauen wir uns ein Negativbeispiel an:
Problematische Variante: Fehlende Smart/Dumb-Trennung
@Component({
selector: 'app-order-summary',
template: \`
<div *ngIf="order()">
<p>Order Total: {{ order().total }}</p>
<button (click)="submitOrder()">Submit Order</button>
</div>
\`
})
export class OrderSummaryComponent {
readonly order = this.orderService.orderSignal;
submitOrder() {
this.orderService.submitOrder(this.order());
}
}
Warum ist dieses Beispiel problematisch?
Die Component rendert UI, enthält aber gleichzeitig die Ablaufsteuerung und greift direkt auf Services zu.
Damit übernimmt sie zwei Verantwortungen: Darstellung und Steuerung.
Sie ist dadurch weder wiederverwendbar noch isoliert testbar.
Wird das Order-Modell erweitert oder geändert, sind zwangsläufig auch UI-Anpassungen erforderlich. Es wurde eine enge Kopplung geschaffen, die vermieden werden sollte.
Wie lässt sich diese Trennung in der Praxis sauber umsetzen? Das folgende Beispiel zeigt einen klaren Ansatz:
Beispiel 2: Klare Trennung von Smart und Dumb
// Smart Component
export class OrderSummaryContainerComponent {
readonly order = this.orderService.orderSignal;
constructor(private orderService: OrderService) {}
submitOrder() {
this.orderService.submitOrder();
}
}
// Dumb Component
@Component({
selector: 'app-order-summary',
template: \`
<div *ngIf="order">
<p>Order Total: {{ order.total }}</p>
<button (click)="submit.emit()">Submit Order</button>
</div>
\`
})
export class OrderSummaryComponent {
@Input() order: Order | null = null;
@Output() submit = new EventEmitter();
}
Hier orchestriert die Container-Component den Ablauf und liefert der Dumb Component (reine Darstellung) die Daten. Die Dumb Component bleibt vollständig frei von Geschäftslogik und API-Interaktionen. Dadurch wird die UI-Schicht klar getrennt und die Testbarkeit deutlich verbessert.
Bei komplexeren Views sorgt diese explizite Trennung für eine klar strukturierte und nachvollziehbare UI-Architektur.
Container- und Presentational-Pattern
Das Container- und Presentational-Pattern ist eine weiterführende Konkretisierung der Smart/Dumb-Trennung. Während „Smart“ und „Dumb“ oft grob verortet werden, definiert dieses Pattern explizit die jeweilige Rolle:
- Container Components steuern den Datenfluss, orchestrieren den Ablauf und verwalten UI-relevante Interaktion mit dem State oder den Services.
- Presentational Components sind rein darstellend, stateless und vollständig konfigurierbar über Inputs und Outputs.
Begriffe im Vergleich: Im Alltag spricht man oft von Smart und Dumb Components, um die Trennung zwischen steuernder und darstellender UI zu beschreiben. Im Architekturkontext ist das Container/Presentational-Pattern die präzisere Variante: Container Components orchestrieren Daten und Logikflüsse, Presentational Components sind rein darstellend und stateless. Beide Begriffe adressieren letztlich dasselbe Prinzip.
An dieser Stelle verzichte ich bewusst auf ein weiteres Beispiel. Der grundlegende Mechanismus wurde im vorherigen Abschnitt bereits demonstriert.
Bei konsequenter Anwendung orchestriert die Container-Component den Datenfluss und steuert die UI-Operationen. Die Presentational Component bleibt rein darstellend und reagiert nur auf Inputs und Events. Dadurch sind Darstellung und Logik klar getrennt und unabhängig weiterentwickelbar.
Dieses Pattern ist besonders wertvoll, um umfangreiche UI-Module klar zu strukturieren und flexibel erweiterbar zu halten.
Architektur Prinzip: Container/Presentational-Pattern ist ein wertvolles Denkmodell, aber kein Dogma. Die explizite Umsetzung lohnt sich überall dort, wo Komponenten wiederverwendbar, testbar oder kontextunabhängig bleiben sollen. Bei klar abgegrenzten, einmalig eingesetzten UI-Components genügt oft eine disziplinierte, sauber strukturierte Single Component. Architektur lebt von bewusstem Entscheiden, nicht von mechanischer Pattern-Anwendung.
Form Components
Form Components sind in vielen Projekten eine typische Stelle, an der sich Architekturfehler einschleichen. Hier vermischen sich UI-Interaktion, Business-Validierung und API-Interaktion besonders leicht. Gerade weil Formulare komplex wirken, verführt es dazu, sämtliche Logik in die Component selbst zu packen. Das widerspricht einer klaren Schichtung und führt schnell zu unwartbarem Code.
Wie sieht das konkret aus? Hier ein Negativbeispiel:
Problematische Variante: Vermischte Logik in der Form Component
export class CheckoutFormComponent {
constructor(private checkoutService: CheckoutService) {}
form = formGroupSignal({
name: formControlSignal('', { validators: [Validators.required] }),
address: formControlSignal('', { validators: [Validators.required] }),
});
submit() {
if (this.form.invalid()) {
alert('Form is invalid!');
return;
}
let data = this.form.value()
if (data.name.startsWith('VIP')) {
data = {
discount: 10,
...data
}
}
this.checkoutService.processOrder(data);
}
}
Warum ist dieses Beispiel problematisch?
Die Component enthält UI-Logik, Validierungsverhalten, Geschäftsregeln und API-Kommunikation. Alles in einer Klasse.
Besonders problematisch ist, dass direkt im UI ein Rabatt regelbasiert berechnet wird.
Damit ist keine Wiederverwendung oder zentralisierte Pflege der Logik möglich.
Ein solcher Ansatz erschwert das Testen und fördert Duplikation in anderen Formularen.
Eine saubere Trennung von UI-Logik und Business-Logik schafft hier deutlich mehr Wartbarkeit:
Beispiel 2: Architekturkonforme Form Component
export class CheckoutFormComponent {
@Output() submitForm = new EventEmitter<CheckoutData>();
form = formGroupSignal({
name: formControlSignal('', { validators: [Validators.required] }),
address: formControlSignal('', { validators: [Validators.required] }),
});
submit() {
if (this.form.invalid()) {
return;
}
this.submitForm.emit(this.form.value());
}
}
// Container Component
export class CheckoutContainerComponent {
constructor(private checkoutService: CheckoutService) {}
handleSubmit(data: CheckoutData) {
this.checkoutService.processOrder(data);
}
}
// CheckoutService (API-Logik gekapselt)
export class CheckoutService {
async processOrder(data: CheckoutData): Promise<void> {
const discount = this.calculateDiscount(data.name);
const payload = {
...data,
discount
};
await this.http.post<void>('/api/checkout', payload).toPromise();
}
private calculateDiscount(name: string): number {
return name.startsWith('VIP') ? 10 : 0;
}
constructor(private http: HttpClient) {}
}
Hier übernimmt die Form Component ausschliesslich das Erfassen und Validieren der UI-Daten. Geschäftslogik und API-Interaktion sind sauber in den CheckoutService ausgelagert und können dort unabhängig getestet und wiederverwendet werden.
Gerade bei wachsenden Formularen ist es sinnvoll, diese Trennung konsequent durchzuhalten. Das reduziert die Komplexität deutlich.
Components und State-Management
State-Management ist einer der häufigsten Stolpersteine in Angular-Projekten. Ohne klare Regeln führt dies schnell zu einer zerklüfteten und schwer nachvollziehbaren State-Logik. UI-State (z.B. aktiver Tab) gehört in die Component. Persistenter oder domänenrelevanter State gehört in eine zentrale State-Schicht. Wer dies nicht bewusst trennt, riskiert inkonsistenten Zustand und schwer wartbare Komponenten.
Wie sieht das konkret aus? Hier ein Negativbeispiel:
Anti-Pattern: Lokale State-Verwaltung in der Component
export class ProductDetailComponent {
product = signal<Product | null>(null);
constructor(private productService: ProductService) {
effect(() => {
this.product.set(this.productService.loadProduct());
});
}
setPrice(price: number) {
const current = this.product();
if (current) {
this.product.set({ ...current, price });
}
}
}
Warum ist dieses Beispiel problematisch?
Die Component lädt Produktdaten und hält sie lokal im UI.
Da der Produktzustand nicht zentral verwaltet wird, ist er für andere Komponenten nicht verfügbar und schwer synchron zu halten.
Gleichzeitig mutiert die Component Daten, die zur Domäne gehören, ohne dass andere UI-Teile dies sehen könnten.
Die lokale Zustandslogik ist weder testbar noch skalierbar.
Bei einem Kunden habe ich komplexe UIs gesehen, bei denen mehrere Modale aufgehen, in sich verschachtelt. Hier wurde auf ein Domänen State verzichtet und versucht mit Input und Output Daten hin und herzureichen. Ein hoffnungsloses Unterfangen. An dieser Stelle wuchs die Komplexität und damit einhergehend auch die Anzahl an Bugs.
Wie gelingt eine bessere Lösung? Das folgende Beispiel zeigt eine saubere Entkopplung:
Beispiel 2: Saubere Trennung von UI-State und Domänen-State
// State Management
export class ProductStateService {
readonly productSignal = signal<Product | null>(null);
constructor(private http: HttpClient) {
this.loadProduct();
}
private loadProduct() {
this.http.get<Product>('/api/products/42').subscribe(product => {
this.productSignal.set(product);
});
}
updatePrice(price: number) {
const product = this.productSignal();
if (product) {
this.productSignal.set({ ...product, price });
}
}
}
// Component
export class ProductDetailComponent {
readonly product = this.productState.productSignal;
constructor(private productState: ProductStateService) {}
setPrice(price: number) {
this.productState.updatePrice(price);
}
}
Hier konsumiert die Component den Domänen-State als Observable. Sie hält selbst keinen State und bleibt dadurch leichter testbar. Änderungen am Produktzustand sind an zentraler Stelle gesteuert, was die Konsistenz im gesamten UI verbessert.
Ein zentrales State-Management erhöht die UI-Konsistenz und reduziert den Pflegeaufwand bei grösseren Anwendungen deutlich.
Components und Use-Case-Interaktion
Components sind nicht für Business-Logik zuständig. Sie interagieren mit Services oder Fassade-Schichten im Core. Wird diese Grenze nicht sauber eingehalten, entsteht schnell enge Kopplung und fehlende Testbarkeit.
Wie sieht das konkret aus? Hier ein Negativbeispiel:
Anti-Pattern: UI-Component übernimmt Business-Flow
export class PaymentComponent {
readonly paymentInProgress = signal(false);
constructor(
private paymentService: PaymentService,
private cartService: CartService,
private http: HttpClient
) {}
pay() {
this.paymentInProgress.set(true);
const cartItems = this.cartService.getItems();
this.http.post('/api/payment', { items: cartItems }).subscribe(() => {
this.paymentInProgress.set(false);
alert('Payment successful!');
});
}
}
Warum ist dieses Beispiel problematisch?
Die Component führt einen kompletten Business-Flow (Zahlung) durch, inklusive API-Aufruf und Fehlerbehandlung.
Die UI ist damit verantwortlich für das fachliche Ergebnis des Prozesses.
Komplexität wie Retry-Logik, Fehlerfälle oder Logging ist schwer zentral steuerbar.
Besonders bei kritischen Prozessen wie Zahlungen gefährdet das die Wartbarkeit und Testbarkeit massiv.
Die bessere Variante delegiert die Business-Logik an einen Service:
Beispiel 2: Saubere Interaktion mit Service
export class PaymentComponent {
readonly paymentInProgress = signal(false);
constructor(private paymentService: PaymentService) {}
pay() {
this.paymentInProgress.set(true);
this.paymentService.processPayment().then(() => {
this.paymentInProgress.set(false);
alert('Payment successful!');
});
}
}
// PaymentService (UseCase-Fassade)
export class PaymentService {
constructor(private http: HttpClient, private cartService: CartService) {}
async processPayment(): Promise<void> {
const cartItems = this.cartService.cartItemsSignal();
await this.http.post<void>('/api/payment', { items: cartItems }).toPromise();
}
}
Hier steuert die Component nur die UI-Interaktion (Progress-Anzeige, Triggern des Prozesses). Die eigentliche Geschäftslogik ist im UseCase-Service gekapselt und kann dort isoliert getestet und weiterentwickelt werden. Dadurch bleibt die Component schlank und auf ihre UI-Verantwortung begrenzt.
Durch diese klare Trennung bleibt die UI-Component fokussiert und die Geschäftsprozesse sind zentral wartbar.
Architektur Prinzip: Weil Components so flexibel sind, verschieben Entwickler ihre Architekturgrenzen unbemerkt. Saubere UI-Architektur heisst: diese Grenze jeden Tag bewusst zu ziehen.
Granularität von Components
Die Wahl der richtigen Granularität von Components ist eine unterschätzte Architekturfrage. Zu grobe Components werden schnell unübersichtlich und schwer testbar. Zu feine Components erzeugen unnötigen Overhead, erschweren die Lesbarkeit und führen zu aufgeblähten Component-Hierarchien. Ziel ist es, für jede Component eine klar abgegrenzte UI-Verantwortung zu definieren.
Wie sieht das konkret aus? Hier ein Negativbeispiel:
Problematische Variante: Überabstrahierte UI-Component
// Button Component (unnötig abstrahiert)
@Component({
selector: 'app-generic-button',
template: \`
<button [type]="type" [disabled]="disabled" (click)="click.emit()">
{{ label }}
</button>
\`
})
export class GenericButtonComponent {
@Input() label!: string;
@Input() type: string = 'button';
@Input() disabled = false;
@Output() click = new EventEmitter();
}
Warum ist dieses Beispiel problematisch?
Die Component abstrahiert ein einfaches HTML-Element ohne echten Mehrwert.
Weder Wiederverwendung noch komplexe Logik rechtfertigen die Abstraktion.
Gleichzeitig entstehen mehr API-Oberflächen (Inputs, Outputs), die gepflegt und getestet werden müssen.
Das erhöht die kognitive Last, ohne funktionalen Nutzen zu bringen.
Eine pragmatische, wartbare Lösung sieht so aus:
Beispiel 2: Sinnvolle Granularität
// Im Template sinnvoll direkt:
<button type="submit" class="btn btn-primary" [disabled]="form.invalid">
Submit Order
</button>
Hier wird ein Button mit klarer UI- und Verhaltenslogik direkt im Template verwendet. Dies ist in der Regel vollkommen ausreichend und besser lesbar. Eine eigene Component sollte nur dann entstehen, wenn zusätzlicher Kontext, komplexes Styling oder spezifische Logik mehrfach wiederverwendet wird.
Architektur Prinzip: Eine Component sollte eine klar abgegrenzte UI-Verantwortung haben und ohne umfangreiche Seiteneffekte eingebunden werden können. Zu feingranulare Components unterbrechen diesen Fluss und erschweren die Wartbarkeit.
Typische Anti-Patterns
Häufige Architekturfehler bei Components:
- Direktes Absetzen von API-Calls aus der Component
- Implementieren von Business-Logik innerhalb der Component
- Komplexes State-Management in der Component selbst
- Fehlende Trennung zwischen Container und Presentational Components
- Form Components, die Validierungen und API-Logik mischen
Diese Muster haben unterschiedliche Auswirkungen, die sich im Alltag schnell bemerkbar machen:
- Direkte API-Calls aus der Component führen zu enger Kopplung und verhindern saubere Trennung der Schichten.
- Business-Logik in der Component erschwert Tests und macht den Code anfällig für unerwartete Seiteneffekte.
- Komplexes State-Management in der Component führt zu inkonsistentem Zustand und schwer nachvollziehbarem Verhalten.
- Fehlende Trennung von Container und Presentational Components macht die UI schwer wiederverwendbar und fragmentiert die Architektur.
- Form Components mit vermischter Logik verhindern Wiederverwendung und führen häufig zu duplizierter, schwer wartbarer Codebasis.
Wer diese Muster konsequent vermeidet und auf klare architektonische Schnitte achtet, schafft eine UI-Schicht, die sich auch bei wachsender Komplexität gut pflegen lässt.
Zusammengefasst: Eine Component ist dann gut gestaltet, wenn ihre Verantwortung klar erkennbar ist. UI-Darstellung und Interaktion gehören in die Component; fachliche Logik, State-Management und API-Kommunikation werden ausgelagert. So bleibt die Anwendung klar, wartbar und skalierbar.
Architektur erfordert kontinuierliche Aufmerksamkeit. Wer die Component-Struktur regelmässig reflektiert und anpasst, verhindert schleichende Verschlechterung und sichert langfristige Qualität.
Im nächsten Beitrag der Serie geht es um Directives. Sie eröffnen vielfältige Gestaltungsmöglichkeiten und erfordern dabei ein bewusstes architektonisches Vorgehen.