Wenn wiederverwendbare React-Komponenten schiefgehen: Prop-Explosion und die Lösung
Sehen Sie, wie eine vorzeitige Wiederverwendung einen einfachen React-Komponenten in eine mit vielen Props belastete Belastung verwandelt – und wie Duplikation, zusammengesetzte Komponenten sowie die Regel der Drei dagegen schützen.
Gemeinsam genutzte Komponenten sollen Zeit sparen, doch in vielen Frontend-Codebasen gibt es eine Komponente, die alle scheuen zu bearbeiten: ein <Modal /> oder <Card /> mit Dutzenden von Eigenschaften, wobei die Behebung eines Fehlers auf einem Bildschirm drei weitere beeinträchtigt. Dieses Ergebnis ist selten das Resultat von Nachlässigkeit; es ist das natürliche Ende einer zu frühen Wiederverwendung der Benutzeroberfläche. Dieser Artikel zeigt, wie eine Komponente zu einer solchen Belastung wird, erklärt, warum die Duplikation der Benutzeroberfläche oft die günstigere Wahl ist, und stellt zwei praktische Werkzeuge zur Vermeidung vor: Kombinationskomponenten und die Regel der Drei.
Wie ein harmloser Shortcut zu einer Komponente mit 32 Eigenschaften wird
Das Muster beginnt in der Regel mit einer vernünftigen Anfrage. Ein Designer übermittelt ein Bezahlfenster mit einem Dialogfeld, das fast dem Bestätigungsdialog auf der Einstellungsseite entspricht. Die Unterschiede sind gering: Die Schaltflächen befinden sich links, der Titel hat ein orangefarbenes Abzeichen daneben, und eine dünne Trennlinie trennt die Schaltflächen vom Inhalt.
Während der Überprüfung weist jemand darauf hin, dass bereits ein <Modal /> existiert, und schlägt vor, dieses zu wiederverwenden. Einige Stunden später kommt ein Pull Request, der vier Eigenschaften hinzufügt: hasOrangeBadge, alignActionsLeft, showDividerLine und badgeText.
Wiederholen Sie das ein Dutzend Mal im Laufe eines Jahres. Das gleiche Modal benötigt nun 32 Props, enthält 15 verschachtelte ternäre Ausdrücke sowie 12 boolesche Flags und stützt sich auf drei useEffect-Hooks, um interne Animationen mit verschiedenen Props-Kombinationen in Einklang zu bringen. Dann passt jemand eine Zeile zur Anpassung des Abstands an, um einen Rechnungsfehler zu beheben, und bricht dadurch versehentlich das Modal auf vier unabhängigen Bildschirmen. Der Versuch, den Code sauber und wiederverwendbar zu halten, hat letztendlich ein Komponentenmodul hervorgebracht, das niemand übernehmen möchte.
Warum DRY bei der UI anders angewandt wird
„Don’t Repeat Yourself“ ist aus gutem Grund ein sinnvoller Rat. Wenn Geschäftslogik wie die Berechnung einer Zahlung oder eine Berechtigungskontrolle dupliziert wird, führt eine Korrektur an einer Kopie dazu, dass die anderen weiterhin fehlerhaft bleiben, und die Kopien entwickeln sich auseinander.
UI-Komponenten ändern sich aus verschiedenen Gründen. Geschäftsregeln ändern sich, wenn sich das Anwendungsfeld verändert. Schnittstellen passen sich an Nutzerprozesse, responsive Brechpunkte, Zugänglichkeitsanforderungen, Produktentscheidungen und Designexperimente an – und diese Faktoren wirken auf jeden Bildschirm unabhängig voneinander ein.
Zwei äußerlich gleich aussehende Elemente sind nicht unbedingt dasselbe Konzept. Das Zusammenführen zweier UI-Muster in eine gemeinsame Komponente, bevor ihre Designs festgelegt sind, führt zu einer Kopplung zwischen Funktionen, die zuvor nicht existierte. Ab diesem Zeitpunkt kann eine kleine Anpassung am Onboarding-Design erneutes Testen von Einstellungen, Abrechnungsprozessen und Analysefunktionen erfordern – einfach weil zufällig dieselbe Komponente dargestellt wird. In diesem Fall wirkt sich die Wiederverwendung gegen Sie aus.
Anatomie der Prop-Explosion
Es hilft, den Entwicklungsverlauf Schritt für Schritt zu verfolgen. Ausgangspunkt ist eine Karte mit einem klaren, minimalen Kontrakt: ein Titel, eine Beschreibung und optional ein Klick-Handler.
interface CardProps {
title: string;
description: string;
onClick?: () => void;
}
Die Implementierung ist genauso kompakt und leicht verständlich:
export function Card({ title, description, onClick }: CardProps) {
return (
<div className="card" onClick={onClick}>
<h3>{title}</h3>
<p>{description}</p>
</div>
);
}
Mit diesem Komponenten gibt es kein Problem. Im vierten Sprint möchte das Marketing Blog-Karten mit einem Bild oben haben, wodurch zwei optionale Bild-Eigenschaften hinzukommen:
interface CardProps {
// ...
imageUrl?: string;
imageAlt?: string;
}
Im siebten Sprint bittet das Dashboard-Team um eine Ecke-Aktionsschaltfläche, die nur bei aktiven Elementen sichtbar ist. Dafür werden ein Flag, ein Icon und ein Handler benötigt:
interface CardProps {
// ...
hasTopRightAction?: boolean;
topRightActionIcon?: React.ReactNode;
onTopRightAction?: () => void;
}
Bis zum zwölften Sprint möchte das Analytics-Team eine zweite Zeile unter dem Titel, ein Status-Abzeichen in drei Farben sowie einen erweiterbaren Fußbereich – was weitere sechs Eigenschaften hinzufügt:
interface CardProps {
// ...
subtitle?: string;
badgeText?: string;
badgeVariant?: "success" | "warning" | "danger";
isExpandable?: boolean;
expandedContent?: React.ReactNode;
defaultExpanded?: boolean;
}
Bis zum 20. Sprint besteht die Render-Funktion aus einer Reihe von Bedingungen. Das Wurzelelement berechnet seine Klasse anhand eines Flags, und das Bild wird nur dann dargestellt, wenn eine URL vorhanden ist:
export function Card(props: CardProps) {
return (
<div
className={`card ${
props.isExpandable ? "card-expandable" : ""
}`}
>
{props.imageUrl && (
<img src={props.imageUrl} alt={props.imageAlt} />
)}
Der Header-Wrapper wird mit dem Titel geöffnet:
<div className="card-header">
<div>
<h3>{props.title}</h3>
folgt auf einen Untertitel, der nur dann angezeigt wird, wenn er bereitgestellt wird:
{props.subtitle && <h4>{props.subtitle}</h4>}
</div>
Die Aktion in der oberen rechten Ecke hängt von einem booleschen Flag ab anstelle davon, ob ein Handler existiert; daher ist es möglich, das Flag zu setzen und das Icon zu vergessen, oder ein Icon zu übergeben und das Flag zu vergessen:
{props.hasTopRightAction && (
<button onClick={props.onTopRightAction}>
{props.topRightActionIcon}
</button>
)}
</div>
Die Auszeichnung erstellt einen Klassennamen aus ihrer Variante und weicht stumm auf eine default-Variante zurück, die vom Typ sogar gar nicht aufgelistet wird:
{props.badgeText && (
<span
className={`badge badge-${
props.badgeVariant || "default"
}`}
>
{props.badgeText}
</span>
)}
Die Beschreibung, das einzige Überbleibsel vom ursprünglichen Design, befindet sich in der Mitte:
<p>{props.description}</p>
Und der erweiterbare Fußteil schließt das Komponentenobjekt ab. Beachten Sie, dass isExpandable sowohl die Wurzelklasse als auch den Fußteil steuert, während defaultExpanded aus der Schnittstelle im Markup überhaupt nicht verwendet wird:
{props.isExpandable && (
<div className="card-footer">
{props.expandedContent}
</div>
)}
</div>
);
}
Solche kleinen Inkonsistenzen sind typisch. Sobald eine Komponente so viele Flags hat, kann niemand alle Kombinationen auf einmal erkennen, wodurch ungültige oder unvollständig implementierte Zustände entstehen. Jeder Nutzer von <Card /> muss sich mit einer immer länger werdenden Liste an Optionen auseinandersetzen und herausfinden, welche Kombinationen unterstützt werden. Die Abstraktion ist nun schwieriger zu verstehen als das einfache Markup, das sie ursprünglich ersetzen sollte.
Duplizierung ist oft günstiger als die falsche Abstraktion
Eine bekannte Aussage aus der Softwareentwicklung, die von Sandi Metz populär gemacht wurde, formuliert es unverblümt: „Duplizierung ist weitaus günstiger als die falsche Abstraktion.“ Bei der Arbeit am Frontend bringt dieser Rat den größten Nutzen.
Wenn zwei Komponenten nur ähnlich sind, ist es oft die sicherere Wahl, sie getrennt zu halten. Nehmen wir an, BillingModal und OnboardingModal sind unabhängige Komponenten:
- Eine Änderung in
BillingModalkannOnboardingModalnicht beeinflussen. - Durch das Entfernen der Onboarding-Funktion wird auch deren Modal sowie die gesamte funktionsspezifische Logik entfernt.
- Jedes Modal entwickelt sich entsprechend seinen eigenen Anforderungen weiter.
- Eine kleine visuelle Änderung erfordert kein Verständnis von Dutzenden von Eigenschaften, die zu anderen Funktionen gehören.
Die Duplikation von JSX kostet nur ein paar zusätzliche Zeilen. Die falsche Abstraktion ist hingegen weitaus teurer – in Bezug auf Zeit für das Debuggen, Regressionstests und die laufende Wartung. Nicht jedes wiederholte Markup-Fragment verdient es, zu einem gemeinsamen Komponenten zu werden.
Komplexe Komponenten: Komposition statt Konfiguration
Echte Wiederverwendung hat weiterhin ihren Platz, insbesondere in einem Design-System. Der Schlüssel liegt darin, wie die gemeinsame Komponente Flexibilität bietet. Anstelle einer einzigen Komponente, die durch eine immer längere Liste von Booleschen Werten gesteuert wird, bietet eine komplexe Komponente eine Reihe kleiner, miteinander verbundener Teile, die von den Nutzern selbst zusammengesetzt werden können.
Hier ist ein auf diese Weise erstelltes Abrechnungsdialogfeld. Die übergeordnete Komponente speichert den offenen Zustand lokal:
export function BillingSettings() {
const [isOpen, setIsOpen] = useState(false);
Die Wurzelkomponente <Modal> erhält nur das, was sie tatsächlich verwaltet – den offenen Zustand sowie einen Callback für Änderungen – und der Überlagerungseffekt ist ein eigenständiger Bestandteil:
return (
<Modal open={isOpen} onOpenChange={setIsOpen}>
<Modal.Overlay />
Der Inhaltsbereich enthält einen Header, und der Header enthält einen Titel:
<Modal.Content>
<Modal.Header>
<Modal.Title>Update Billing Plan</Modal.Title>
Ein Badge ist einfach ein weiteres Kind des Headers, wobei seine Variante als Eigenschaft ausschließlich am Badge selbst angegeben wird:
<Modal.Badge variant="warning">
Action Required
</Modal.Badge>
</Modal.Header>
Der Körper enthält beliebigen Inhalt, beginnend mit einer Nachricht:
<Modal.Body>
<p>
Please update your payment method to avoid account suspension.
</p>
und setzt sich fort mit einem für eine bestimmte Funktion vorgesehenen Formular, von dem das Modal selbst nichts weiß:
<CreditCardForm />
</Modal.Body>
Der Fußbereich steuert seine eigene Ausrichtung und enthält gewöhnliche Schaltflächen, von denen die erste das Dialogfeld schließt:
<Modal.Footer align="right">
<Button
variant="ghost"
onClick={() => setIsOpen(false)}
>
Cancel
</Button>
Die primäre Aktion vervollständigt den Fußbereich sowie die gesamte Struktur:
<Button variant="primary">
Save Changes
</Button>
</Modal.Footer>
</Modal.Content>
</Modal>
);
}
Der strukturelle Unterschied ist wichtig. Wenn ein Modal ein Abzeichen benötigt, gibt es keine showBadge-Eigenschaft hinzuzufügen; man rendernt stattdessen <Modal.Badge />. Wenn ein Bildschirm individuellen Inhalt benötigt, platziert man diesen an der richtigen Stelle, anstatt eine weitere Flagge zu erfinden. Die Vorteile sind sofort offensichtlich:
- Keine Überlastung durch Eigenschaften. Ein Modal ohne Abzeichen oder Fußbereich rendernt einfach weder
<Modal.Badge />noch<Modal.Footer />. - Mehr Flexibilität. Ein Icon neben dem Titel wird innerhalb von
<Modal.Header>platziert; es ist keine neue Eigenschaft erforderlich. - Isolierter Stil. Änderungen an
<Modal.Badge />müssen den Kerncontainer nicht beeinflussen.
Hinter den Kulissen teilen Kombinationskomponenten in der Regel Zustände wie open über React Context, wodurch <Modal.Footer> oder ein Schließen-Button darauf zugreifen können, ohne Eigenschaften mehrfach weitergeben zu müssen. Die Komposition überträgt die Steuerung an den Verwender, ohne die Komponente in ein Konfigurationsobjekt zu verwandeln. Dieser Ansatz ist jedoch nicht kostenlos: Das Design-System muss dokumentieren, welche Teile wo eingebettet werden dürfen, und die Verwender müssen bei jeder Nutzung etwas mehr Markup schreiben. Für eine weitere Sichtweise auf diese Refaktorierung siehe unseren Leitfaden zu der Behebung von Eigenschaftsüberlastung mit Komposition und Slots.
Die Regel der Drei für das Extrahieren gemeinsamer Komponenten
Eine einfache Heuristik hilft dabei zu entscheiden, wann eine Abstraktion gerechtfertigt ist: Warten Sie auf das dritte tatsächliche Vorkommen.
Erstes Vorkommen: Schreiben Sie es direkt ein
Fügen Sie die Markierung direkt in den Ansichtsteil ein, der sie benötigt. Widerstehen Sie dem Drang zur Abstraktion und belassen Sie Styles sowie Struktur in der Nähe der jeweiligen Funktion.
Zweites Vorkommen: Kopieren und anpassen
Wenn ein weiteres Bildschirmformat etwas Ähnliches benötigt, ist es sehr verlockend, ein globales Komponent zu erstellen. Kopieren Sie stattdessen die Markierung und passen Sie sie an den neuen Kontext an. Nun haben Sie zwei konkrete Beispiele, und im Laufe der Zeit können Sie beobachten, wo sie sich tatsächlich unterscheiden, anstatt über zukünftige Anforderungen zu spekulieren.
Drittes Vorkommen: Extrahieren mit Belegen
Wenn ein dritter, separater Bildschirm dasselbe visuelle und verhaltensmäßige Muster benötigt, hat man endlich genügend Hinweise darauf, was tatsächlich gemeinsam ist und was sich unterscheidet. Dieses lange Warten macht die wahren Invarianten sichtbar – also die Teile, die immer gleich bleiben – sowie die tatsächlichen Variationen, die flexibel bleiben müssen. Solche Variationen eignen sich gut als Kompositionsslots anstelle von booleschen Eigenschaften.
Ziel ist es nicht, wiederverwendbare Komponenten zu vermeiden. Es geht vielmehr darum, Abstraktionen nicht auf Annahmen zu basieren.
Kernpunkte
- Achten Sie auf die Vermehrung von Eigenschaften. Wenn eine Komponente ständig weitere Konfigurations-eigenschaften für unzusammenhängende Anwendungsfälle erhält, hinterfragen Sie die Abstraktion, bevor Sie weitere hinzufügen.
- Ziehen Sie Komposition der Konfiguration vor. Kinderkomponenten, Slots und zusammengesetzte Komponenten bieten Flexibilität, ohne jedes Mal neue boolesche Eigenschaften einfügen zu müssen.
Eine gute Frontend-Architektur wird nicht anhand der geringsten Anzahl an Codezeilen bemessen, sondern daran, wie sicher sich der Code ändern lässt, ohne dass dies Auswirkungen auf die gesamte Anwendung hat. Manchmal ist die beste Komponente nicht die, die überall wiederverwendet wird, sondern die, die in Ruhe gelassen wird.
Verwandte Literatur
- React-Prop-Drilling und „God Components“ auf ein angemessenes Maß reduzieren — Lernen Sie sieben konkrete Refactoring-Muster, um überdimensionierte React-Komponenten durch die Isolierung von State, Datenabruf, Berechtigungen und Ladelogik statt nur durch das Aufteilen von Dateien zu zerlegen.
- React-Prop-Überlastung mit Composition und Slots beheben — Erfahren Sie, warum konfigurationsintensive React-Props zu Wartungsaufwänden führen, und wie Inversion of Control, Composition sowie Slots wirklich wiederverwendbare Komponenten erzeugen.