Eine der zentralen Fertigkeiten in React ist nicht das Schreiben einzelner Komponenten – es ist das Zerlegen einer fertigen UI in eine sinnvolle Komponenten-Hierarchie. Wer das gut macht, baut wartbare, wiederverwendbare und gut testbare Anwendungen. Wer es schlecht macht, landet bei tausendzeiligen Komponenten, die niemand mehr anfassen will. Dieser Artikel führt durch das fünfstufige Vorgehen aus der offiziellen React-Doku und zeigt, woran man eine gute Komponenten-Aufteilung erkennt.

Das Beispiel-Mockup

Stell dir folgende UI vor: eine Produkttabelle mit Suchfeld und einer Checkbox „nur lagernde Artikel anzeigen". Darunter Zeilen mit Produkten, gruppiert nach Kategorie.

text Vereinfachtes Mockup
┌────────────────────────────────────────┐
│  [Suche...]    [☐ Nur lagernd]        │
├────────────────────────────────────────┤
│  Frucht                                │
│    Apfel                          1,50 │
│    Birne                          2,00 │
│  Gemüse                                │
│    Karotte                        0,80 │
│    Tomate                         1,20 │
└────────────────────────────────────────┘

Daten kommen als Liste von Produkten:

TypeScript Beispiel-Daten
const products = [
    { category: 'Frucht', name: 'Apfel',  price: '1,50', stocked: true },
    { category: 'Frucht', name: 'Birne',  price: '2,00', stocked: false },
    { category: 'Gemüse', name: 'Karotte',price: '0,80', stocked: true },
    { category: 'Gemüse', name: 'Tomate', price: '1,20', stocked: true },
];

Schritt 1 – Hierarchie zeichnen

Zeichne (im Kopf oder auf Papier) Rahmen um jede Komponente. Eine Komponente sollte idealerweise eine Sache tun. Kriterien zur Abgrenzung:

  • Single Responsibility – Wenn eine Box logisch zusammengehört, ist sie ein Kandidat.
  • CSS – Was du sowieso als eigene CSS-Klasse stylen würdest, ist oft auch eine Komponente.
  • Daten – Bekommt ein Bereich genau ein Objekt als Input, ist das ein guter Schnitt.

Für das Mockup ergibt sich:

text Komponenten-Hierarchie
FilterableProductTable      (Wurzel-Komponente)
├── SearchBar               (Suche + Checkbox)
└── ProductTable
    ├── ProductCategoryRow  (Kategorie-Header)
    └── ProductRow          (eine Produkt-Zeile)

Diese Hierarchie ist die spätere Datei- und Import-Struktur.

Schritt 2 – Statische Version bauen

Bevor du an Interaktivität denkst, baue die Komponenten ohne State – nur mit Props. Daten fließen von oben nach unten. Es soll genau das angezeigt werden, was die Daten hergeben.

TypeScript ProductTable.jsx
function ProductTable({ products }) {
    const rows = [];
    let lastCategory = null;

    products.forEach((product) => {
        if (product.category !== lastCategory) {
            rows.push(
                <ProductCategoryRow
                    category={product.category}
                    key={product.category}
                />
            );
        }
        rows.push(<ProductRow product={product} key={product.name} />);
        lastCategory = product.category;
    });

    return (
        <table>
            <thead>
                <tr><th>Name</th><th>Preis</th></tr>
            </thead>
            <tbody>{rows}</tbody>
        </table>
    );
}

Wichtig: In dieser Phase kein useState, kein Event-Handler. Nur Daten ⇒ JSX. Die statische Variante zwingt dich, die Hierarchie und Datenflüsse sauber zu durchdenken.

Schritt 3 – Minimalen State finden

Jetzt kommt die wichtigste Frage: Was ist State?

State ist nur das, was sich über die Zeit ändert und sich nicht aus anderen Daten berechnen lässt. Alles, was abgeleitet werden kann, ist kein State.

Geh die Daten durch und stelle drei Fragen:

  1. Bleibt der Wert über die Zeit gleich? -> Konstante, kein State.
  2. Wird er von einem Elternteil als Prop übergeben? -> Prop, kein State.
  3. Lässt er sich aus anderen Werten berechnen? -> Abgeleitet, kein State.

Bleibt nichts übrig: dann ist es State.

Für unser Beispiel:

WertState?
Originale Produktliste− Prop von außen
Eingegebener Suchtext+ State (ändert sich, nicht ableitbar)
Checkbox „nur lagernd"+ State (ändert sich, nicht ableitbar)
Gefilterte Liste− Berechnet aus Liste + Suchtext + Checkbox

Es bleiben also zwei State-Werte übrig. Das ist das Minimum.

Schritt 4 – State auf die richtige Ebene heben

Jetzt steht die nächste Frage: Wo lebt der State?

Vorgehen:

  1. Welche Komponenten lesen den State? SearchBar (zeigt Suchtext) und ProductTable (filtert damit).
  2. Welche Komponente ändert ihn? SearchBar (Eingaben).
  3. Suche den niedrigsten gemeinsamen Vorfahren, der beide Komponenten umschließt – das ist FilterableProductTable.

Dort kommt der State hin:

TypeScript FilterableProductTable.jsx
import { useState } from 'react';

function FilterableProductTable({ products }) {
    const [filterText, setFilterText] = useState('');
    const [inStockOnly, setInStockOnly] = useState(false);

    return (
        <div>
            <SearchBar
                filterText={filterText}
                inStockOnly={inStockOnly}
            />
            <ProductTable
                products={products}
                filterText={filterText}
                inStockOnly={inStockOnly}
            />
        </div>
    );
}

Tieferes zum Thema: State Lifting.

Schritt 5 – Datenfluss umkehren

Die Filterwerte werden gelesen, aber noch nicht verändert. Damit die SearchBar den State des Eltern-Components ändern kann, übergibt das Elternteil Setter-Funktionen als Props nach unten – das Kind ruft sie bei Events auf.

TypeScript FilterableProductTable.jsx
<SearchBar
    filterText={filterText}
    inStockOnly={inStockOnly}
    onFilterTextChange={setFilterText}
    onInStockOnlyChange={setInStockOnly}
/>
TypeScript SearchBar.jsx
function SearchBar({
    filterText,
    inStockOnly,
    onFilterTextChange,
    onInStockOnlyChange,
}) {
    return (
        <form>
            <input
                type="text"
                value={filterText}
                placeholder="Suche..."
                onChange={(e) => onFilterTextChange(e.target.value)}
            />
            <label>
                <input
                    type="checkbox"
                    checked={inStockOnly}
                    onChange={(e) => onInStockOnlyChange(e.target.checked)}
                />
                Nur lagernde Artikel
            </label>
        </form>
    );
}

Damit ist das Grundprinzip von React in einem Bild:

  • Props fließen nach unten.
  • Events fließen nach oben.
  • State lebt so weit oben wie nötig, so weit unten wie möglich.

Heuristiken für gute Komponenten-Aufteilung

  • 20–80 Zeilen pro Komponente sind ein gesunder Bereich. Längeres lässt sich meist sinnvoll aufteilen.
  • Eine Komponente sollte einen Satz beschreiben können: „Zeigt eine einzelne Produktzeile." Wenn dein Satz „und" enthält, ist es vermutlich zu viel.
  • Wiederverwendung ist nicht das oberste Ziel. Manche Komponenten existieren nur einmal – das ist okay, solange sie eine klare Aufgabe haben.
  • Nicht zu früh abstrahieren. Erst wenn dasselbe Muster zum dritten Mal auftaucht, lohnt sich eine generische Komponente.

Häufige Anti-Patterns

Den State zu hoch liften.

Wenn nur ein einziges Kind den State braucht, gehört er dort hin – nicht in den Großeltern-Container.

Abgeleitete Werte als State speichern.

Sobald du useEffect schreibst, um eine state aus einem anderen state synchron zu halten, hast du ein Problem. Berechne den Wert einfach beim Rendern.

Riesige App-Komponenten.

Wenn deine App.jsx 600 Zeilen lang ist und überall State und Effekte mischt, fehlt die Hierarchie.

/ Weiter

Zurück zu Grundlagen

Zur Übersicht