Wenn es ein Pattern gibt, das ich in jedem Team durchsetze, das ich leite, dann ist es die Trennung zwischen Smart und Dumb Components. Es klingt einfach. In der Praxis ist es die einzelne wirkungsvollste Architekturentscheidung, die man in einer Frontend-Codebase treffen kann — und Teams, die sie ignorieren, zahlen den Preis in jedem Sprint.
Die Idee ist nicht neu, aber ich stelle fest, dass sie konsequent missverstanden wird. Es geht nicht nur um «Komponenten mit State» versus «Komponenten ohne State». Es geht um eine fundamentale Trennung von Verantwortlichkeiten, die bestimmt, wie wiederverwendbar, testbar und wartbar der Code tatsächlich ist.
Was ist eine Dumb Component?
Eine Dumb Component ist generisch und vollständig agnostisch gegenüber der Geschäftslogik. Sie weiss nicht, woher ihre Daten kommen. Sie ruft nichts ab. Sie weiss nicht, was eine Postleitzahl ist, was ein Warenkorb tut, oder welche API das Backend bereitstellt. Sie empfängt Daten über Props, rendert UI und meldet Benutzerinteraktionen über Callbacks zurück. Das ist alles.
Stell dir sie als eine reine Funktion für UI vor: gleicher Input, gleicher Output, jedes Mal. Eine SelectableList-Komponente kümmert sich nicht darum, ob sie Postleitzahlen, Produktkategorien oder Ländernamen rendert. Ihr Interface ist in generischen Konzepten definiert — Items mit Labels und Values, ein Selection-Callback, vielleicht ein Platzhaltertext.
Wichtige Nuance: Dumb Components können andere Dumb Components enthalten. Eine PostalCodeList kann eine Dumb Component sein, die eine SelectableList mit zusätzlicher UI aggregiert — ein Label, eine Länderflagge, Layout. Sie transformiert keine Daten und mappt keine Models. Sie komponiert nur. Die Smart Component darüber ist diejenige, die rohe API-Daten abruft und sie in die Form mappt, die die Dumb erwartet. Das ist die Trennung: Smart transformiert das Model, Dumb aggregiert die Darstellung.
Das ist der entscheidende Punkt: Die Dumb Component definiert ihr eigenes Interface. Sie ist der Provider. Sie sagt «gib mir Items in dieser Form, und ich rendere sie.» Der Parent muss seine Daten anpassen, nicht umgekehrt.
// Dumb component: generic, reusable, knows nothing about postal codes
interface SelectableListProps {
items: { label: string; value: string }[];
onSelect: (value: string) => void;
placeholder?: string;
}
export const SelectableList: FC<SelectableListProps> = ({
items,
onSelect,
placeholder = 'Select...',
}) => (
<ul role="listbox" aria-label={placeholder}>
{items.map((item) => (
<li key={item.value} role="option" onClick={() => onSelect(item.value)}>
{item.label}
</li>
))}
</ul>
);
// Also dumb! Aggregates SelectableList with postal-code-specific UI
// Adds a label and flag — but does NOT transform data, only composes
export interface PostalCodeListProps {
items: SelectableListProps['items'];
onSelect: (value: string) => void;
label?: string;
countryFlag?: string;
}
export const PostalCodeList: FC<PostalCodeListProps> = ({
items,
onSelect,
label = 'Postal code',
countryFlag,
}) => (
<div>
<span>{countryFlag} {label}</span>
<SelectableList items={items} onSelect={onSelect} placeholder={label} />
</div>
);Was ist eine Smart Component?
Eine Smart Component ist der Consumer. Sie kennt die Business-Domäne. Sie ruft Daten von APIs ab, verwaltet State, behandelt Seiteneffekte und transformiert Rohdaten in die Form, die Dumb Components erwarten. Sie ist das Bindeglied zwischen Backend und UI.
Ein PostalCodePicker ist eine Smart Component. Er weiss, dass Postleitzahlen existieren, dass sie von einer API kommen, und dass sie nach Land gefiltert werden. Er ruft die Rohdaten ab und mappt sie in das generische Items-Format, das die Dumb PostalCodeList erwartet. Die Dumb Component sieht nie dein API-Model — sie sieht nur saubere, vorher transformierte Props. Deshalb bleibt die Dumb wiederverwendbar: Sie hat keine Ahnung, woher die Daten kamen oder welche Form sie vorher hatten.
// Smart component: fetches data AND transforms the model for the dumb component
import { PostalCodeList } from './PostalCodeList';
export const PostalCodePicker: FC<{ country: string }> = ({ country }) => {
const [codes, setCodes] = useState<PostalCode[]>([]);
useEffect(() => {
fetchPostalCodes(country).then(setCodes);
}, [country]);
// Smart transforms raw API data → shape the dumb component expects
const items = codes.map((c) => ({
label: `${c.code} — ${c.city}`,
value: c.code,
}));
return (
<PostalCodeList
items={items}
onSelect={(code) => updateAddress({ postalCode: code })}
countryFlag={getFlagEmoji(country)}
/>
);
};Die Provider-Analogie
Ich vermittle dies als Provider/Consumer-Pattern. Die Child-Komponente ist der Provider — sie bietet ein sauberes, klar definiertes Interface. Die Parent-Komponente ist der Consumer — sie konsumiert dieses Interface, indem sie ihre Daten entsprechend transformiert.
Das kehrt um, wie die meisten Entwickler über Komponentenbeziehungen denken. Der natürliche Instinkt ist «der Parent entscheidet, was das Child bekommt.» Aber in gut architektoniertem Code deklariert das Child, was es braucht, und der Parent ist für die Anpassung verantwortlich. Der Parent transformiert Modelldaten zum Props-Interface des Child. Der Parent ist für das Layout verantwortlich — Margin, Gap, Grid, Flex.
Warum ist das wichtig? Weil wenn das Child den Vertrag definiert, das Child generisch bleibt. Es kann überall wiederverwendet werden. Aber wenn der Parent die Form diktiert, wird das Child an einen spezifischen Anwendungsfall gekoppelt.
// The child (dumb) defines the interface — it is the PROVIDER
// The parent (smart) adapts data to that interface — it is the CONSUMER
// ❌ Wrong: dumb component receives raw API data and maps it internally
<PostalCodeList codes={rawApiResponse} format="CH" />
// Now PostalCodeList has to know how to transform your API model — it's coupled
// ✅ Right: smart transforms, dumb aggregates
// Smart maps the model to the shape the dumb expects
const items = apiResponse.map((c) => ({ label: `${c.code} — ${c.city}`, value: c.code }));
// Dumb just composes other dumb components (label + SelectableList)
<PostalCodeList items={items} onSelect={handleSelect} countryFlag="🇨🇭" />Warum diese Trennung alles verändert
Storybook und Dokumentation: Dumb Components leben wunderbar in Storybook. Du übergibst Props, du siehst das Ergebnis. Keine gemockten APIs, keine Provider-Wrapper, kein Authentifizierungs-Context. Das bedeutet, Designer können Komponenten isoliert reviewen, QA kann Edge Cases testen ohne vollständige Umgebungen aufzusetzen, und neue Entwickler können die Komponentenbibliothek durchstöbern und verstehen, was verfügbar ist.
Geteilter Code über Projekte hinweg: Dumb Components sind per Definition portabel. Diese SelectableList funktioniert in deinem E-Commerce-Checkout, deinem Admin-Dashboard und deiner mobilen App. Weil sie nichts über dein Business weiss, gehört sie allen. Wir legen diese in eine geteilte Komponentenbibliothek und sie werden echte wiederverwendbare Assets statt Copy-Paste-Vorlagen.
Testing ist unkompliziert: Eine Dumb Component zu testen ist trivial — rendern mit Props, Output überprüfen. Kein API-Mocking, kein asynchroner State zum Warten, keine Context-Provider zum Wrappen. Eine Smart Component zu testen ist ebenfalls sauberer, weil du nur die Datenlogik und Orchestrierung testest, nicht die Rendering-Details.
Refactoring ohne Angst: Wenn sich die API-Form ändert, aktualisierst du die Transformationslogik der Smart Component. Die Dumb Component ändert sich nicht. Wenn das Design-Team das Listen-Styling aktualisiert, aktualisierst du die Dumb Component. Die Smart Component ändert sich nicht. Änderungen sind isoliert, weil Verantwortlichkeiten isoliert sind.
Wann aufteilen
Man beginnt nicht aufgeteilt. Man beginnt mit allem in einer Komponente. Das ist wichtig — vorzeitiges Aufteilen ist nur vorzeitige Abstraktion in anderem Gewand. Das knüpft direkt an das YAGNI-Prinzip an, über das ich in Warum YAGNI besser ist als DRY geschrieben habe. Extrahiere keine Dumb Component, weil du denkst, du könntest sie irgendwann wiederverwenden. Extrahiere sie, wenn du sie tatsächlich brauchst.
Ich teile auf, wenn eines davon zutrifft: Die Komponente ruft Daten ab UND rendert komplexe UI, der visuelle Teil könnte in einem anderen Kontext mit anderen Daten wiederverwendet werden, die Datei wird zu gross (was oft auf gebrochene Single Responsibility hindeutet), oder ich will diese Komponente in Storybook.
HTML, das rein strukturell ist — ein Wrapper-Div für Layout, ein Section-Tag für Semantik — braucht keine eigene Komponente. Extrahiere Dinge nicht nur, weil sie HTML sind. Extrahiere Dinge, weil sie ein sinnvolles, wiederverwendbares UI-Stück mit klarem Interface darstellen.
Der echte Gewinn
Teams, die diese Trennung durchsetzen, liefern über die Zeit schneller. Nicht an Tag eins — an Tag eins ist alles inline schreiben schneller. Aber ab Monat drei hat das Team mit sauberer Trennung eine Bibliothek getesteter, dokumentierter Dumb Components, die sie in Stunden statt Tagen zu neuen Features zusammensetzen können. Das Team ohne hat einen Haufen verwickelter Smart Components, wo jede Änderung riskiert, etwas Unbeteiligtes zu brechen.
Es geht nicht um architektonische Reinheit. Es geht darum, das zweite Feature günstiger als das erste zu machen. Das ist der ganze Sinn guter Architektur — die Kosten der Änderung zu reduzieren. Und kein Pattern tut das zuverlässiger, als die Geschäftslogik aus den UI-Komponenten rauszuhalten.

