Die Aufteilung in Smart- (Container) und Dumb-Komponenten (Presentational, „UI“) ist die nützlichste Architekturregel für Komponenten. Sie beantwortet die Frage: Wer darf wissen, woher die Daten kommen? Die Antwort: so wenige Komponenten wie möglich.
| Smart / Container | Dumb / Presentational | |
|---|---|---|
| Rolle | Orchestrierung: holt Daten, verarbeitet Aktionen | Darstellung: zeigt Daten, emittiert Absichten |
| Injiziert | Stores, API-Services, Router | nichts Fachliches (nur UI: ElementRef, DestroyRef…) |
| Kommunikation | Store / Services | nur input(), output(), model() |
| Zustand | fachlicher Zustand (oder an einen Store delegiert) | nur lokaler UI-Zustand (Hover, aufgeklappt) |
| Wiederverwendung | selten — an einen Anwendungsfall gebunden | häufig — überall, wo du dieselben Daten hast |
| Testen | mit Mocks / einem Fake-Store | trivial: du gibst Inputs vor und prüfst Outputs |
| Wo sie liegt | features/x/pages | features/x/ui oder shared/ui |
Ein vollständiges Beispiel: ein Produktkatalog
Eine Smart-Seite (CatalogPage) mit einem Feature-Store (CatalogStore) und drei Dumb-Komponenten: den Filtern, der Produktkarte und der Warenkorb-Zusammenfassung. Die Daten kommen über httpResource vom simulierten Backend.
// SMART (Container): weiß, WOHER die Daten kommen und WAS bei einer Aktion passiert.
@Component({
selector: 'app-catalog-page',
imports: [ProductFilters, ProductCard, CartSummary],
providers: [CatalogStore], // der Store lebt so lange wie die Seite
template: `
<app-product-filters [categories]="categories" [(query)]="store.query" [(category)]="store.category" />
@for (p of store.products.value(); track p.id) {
<app-product-card [product]="p" (addToCart)="store.add($event)" />
}
<app-cart-summary
[lines]="store.cart()" [count]="store.cartCount()" [total]="store.cartTotal()"
(remove)="store.remove($event)" />
`,
})
export class CatalogPage {
protected readonly store = inject(CatalogStore);
protected readonly categories: ProductCategory[] = ['laptop', 'phone', 'audio', 'accessory'];
}Wo die Dateien liegen
features/catalog/
├── catalog.routes.ts
├── pages/
│ └── catalog-page.ts ← SMART (geroutet)
├── data-access/
│ ├── catalog-store.ts ← Zustand + Daten
│ └── catalog-api.ts
├── ui/ ← DUMB, featurespezifisch
│ ├── product-card.ts
│ ├── product-filters.ts
│ └── cart-summary.ts
└── models/
└── product.ts ← Product + toProduct(dto)
shared/ui/ ← DUMB, generisch (ohne Domäne)
├── button.ts
├── rating.ts
└── data-table.tsAnti-Pattern: die „Dumb“-Komponente, die in Wahrheit Smart ist
// ✗ Eine „Dumb“-Komponente, die in Wahrheit eine verkappte Smart-Komponente ist
@Component({ selector: 'app-product-card', /* ... */ })
export class ProductCard {
private readonly cart = inject(CartStore); // ← Abhängigkeit von globalem Zustand
private readonly http = inject(HttpClient); // ← sendet Requests
private readonly router = inject(Router); // ← entscheidet über die Navigation
readonly productId = input.required<number>();
readonly product = httpResource<Product>(() => `/api/products/${this.productId()}`);
add() {
this.cart.add(this.product.value()!);
this.router.navigate(['/cart']);
}
}
// Folgen: Du kannst sie nicht auf einer anderen Seite wiederverwenden (z. B. in „Empfehlungen“ oder im Admin-Bereich),
// du kannst sie nicht ohne Mocks für HttpClient, Router und CartStore testen,
// und eine Liste mit 50 Karten sendet 50 Requests.Was eine Dumb-Komponente darf
// ✓ Lokaler UI-Zustand in einer Dumb-Komponente — völlig in Ordnung
export class Accordion {
readonly items = input.required<Item[]>();
protected readonly expanded = signal<number | null>(null); // betrifft nur die Darstellung
}
// ✓ „UI“-Injektionen in einer Dumb-Komponente — in Ordnung
inject(ElementRef); inject(DestroyRef); inject(LOCALE_ID); inject(Dialog /* CDK */);Praktische Regeln
- Eine geroutete Seite = ein Smart-Container. Er verbindet die URL, den Store und die UI.
- Dumb-Komponenten injizieren keine Stores oder Daten-Services. Wenn du das Bedürfnis spürst, hebe die Logik eine Ebene höher.
- Outputs drücken Absichten aus, keine Implementierungen:
(addToCart), nicht(callCartServiceAndNavigate). - Nicht alles muss Dumb sein. Ein komplexes Widget innerhalb einer Seite (z. B. „Kommentare“) kann ein Mini-Smart-Container mit eigenem Store sein — besser, als 15 Inputs durch 4 Ebenen zu reichen.
- Prop Drilling über 2–3 Ebenen ist das Signal, dass du einen Zwischen-Container oder einen Feature-Store brauchst.
Der State einer Smart-Komponente liegt meist in einem Store: State Management mit Signals. Wie beide miteinander reden, zeigt input() und output().
Wann ja, wann nein
- Sie zeigt übergebene Daten an (Karte, Tabellenzeile, Avatar, Badge).
- Sie ist ein Steuerelement (Filter, Bewertung, Toggle) — der Zustand kommt über
model(). - Du willst sie auf einer anderen Seite oder in
shared/uiwiederverwenden. - Du willst sie einfach testen oder in Storybook zeigen.
- Sie ist eine geroutete Seite.
- Sie ist ein eigenständiges Widget mit eigenen Daten (Chat, Benachrichtigungen), das „Drop-in“ auf mehreren Seiten verwendet wird.
- Sie Dumb zu machen würde 10+ Inputs/Outputs erfordern, die durch mehrere Ebenen gereicht werden.