Zerlegen von zu großen React-Komponenten ohne Produktionsrückfälle
Ein schrittweiser Ansatz zur sicheren Aufteilung großer React-Komponenten: Zuweisung von Verantwortlichkeiten, Extraktion der reinen Logik, Übernahme des Zustands sowie Bereitstellung in kleinen Schritten.
Quellen: React Components Are Getting Too Big: How to Refactor Without Breaking Production
Jede ausgereifte React-Codebasis besitzt so etwas: eine Komponente, die funktioniert, Umsatz einbringt und von allen, die sie bearbeiten müssen, gefürchtet wird. Sie begann als einfaches Display, erhielt anschließend Filterfunktionen, Paginierung, API-Aufrufe, Berechtigungen, Validierungen, Dialoge, Exportfunktionen, Analysewerkzeuge sowie eine immer größere Anzahl an Effekten. Diese Anleitung zeigt, wie man solche Komponenten durch Reduzierung ihrer Komplexität – und nicht nur ihrer Dateigröße – verkleinern kann, sowie wie man dies schrittweise tun sollte, ohne das Produktionsverhalten zu gefährden.
Ein typisches Beispiel hat stillschweigend alle Aspekte der jeweiligen Funktion integriert:
function ProductManagement() {
// API calls
// Redux selectors
// Local state
// Filters
// Pagination
// Sorting
// Selection
// Permissions
// Validation
// Business rules
// Export
// Modal handling
// Analytics
// Hundreds of lines of JSX
}
Bei tausend Zeilen oder mehr scheint das offensichtliche Problem die Länge zu sein. Das ist jedoch nicht der Fall. Ein langer Komponentencode ist nicht automatisch ein Problem – ein Komponentencode mit vielen unzusammenhängenden Gründen für Änderungen schon. Die unten beschriebene Arbeit dreht sich nicht darum, eine Datei in zehn zu teilen, sondern darum, die Kosten und das Risiko der nächsten Änderung zu verringern.
Die Zeilenanzahl ist die falsche Kennzahl
Regeln wie „Refaktorisiere alles mit mehr als 300 Zeilen“ sind leicht umzusetzen, aber unzureichend, um echte Risiken aufzuzeigen. Stellen Sie sich 500 Zeilen hauptsächlich deklarativer Markup-Strukturen mit wenig Logik vor. Jetzt stellen Sie sich 200 Zeilen vor, die API-Aufrufe, Zustandswechsel, Berechtigungsprüfungen, Validierungen, Effekte, Geschäftsregeln sowie verschachtelte bedingte Darstellungen kombinieren. Die kürzere Datei ist fast sicherlich die schwierigere, um sie sicher zu ändern.
Wichtig ist die kognitive Last. Eine nützliche Frage: Wie viel Kontext benötigt ein Teammitglied, bevor es dieses Komponenten mit Sicherheit ändern kann?
Symptome dafür, dass Komplexität zu einem Nachteil wird
Achten Sie auf diese Anzeichen:
- mehrere unzusammenhängende Aufrufe von
useEffect - eine lange Liste unabhängiger Zustandsvariablen
- API-Anfragen, die direkt von UI-Komponenten gestartet werden
- Business-Regeln, die in Click-Handler versteckt sind
- tief verschachtelte bedingte Darstellungen
- Genehmigungslogik, die mit der Präsentation vermischt ist
- dieselbe Datenumwandlung, die an mehreren Stellen wiederholt wird
- verschiedene Verantwortlichkeiten, die denselben Zustand lesen und schreiben
- lange Ketten von Props, die über Zwischenkomponenten weitergeleitet werden
- doppelte Netzwerkanfragen
- Tests, die eine ganze Seite initialisieren müssen, um nur eine kleine Regel zu überprüfen
Das auffälligste Symptom ist eher sozial als technisch: Die Leute meiden die Datei. Sobald „Fassen Sie das nicht an, es geht immer etwas kaputt“ zum Team-Märchen wird, ist das Komponentenproblem kein Stilproblem mehr, sondern ein Ingenieur-Risiko.
Definieren Sie die Verantwortlichkeiten, bevor Sie den Code anfassen
Der erste Schritt besteht nicht darin, JSX im Editor auszuziehen. Es geht darum, aufzuschreiben, was die Komponente tatsächlich tut. Für den Produktmanagement-Bildschirm könnte das Inventar so aussehen:
ProductManagement
│
├── Fetch products
├── Transform API response
├── Filter products
├── Sort products
├── Manage selection
├── Validate permissions
├── Export data
├── Open/close dialogs
├── Handle errors
└── Render UI
Diese Liste zeigt bereits, dass die Komponente gleichzeitig mehrere Rollen übernimmt. Nach Art gruppiert fallen die Verantwortlichkeiten in Daten, Verhalten und Präsentation:
ProductManagement
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
Data Behavior Presentation
│ │ │
▼ ▼ ▼
API Filters/Rules Table
State Selection Dialog
Validation Actions
Genau bei dieser Gruppierung beginnt eine gute Refaktorierung. Zuerst suchen Sie nach Grenzen, erst danach nach Dateien.
Ziel: Schichten mit klarem Eigentumsverhältnis
Das Aufteilen einer großen Datei in fünf kleinere ändert die Struktur auf der Festplatte, nicht jedoch das Design:
BigComponent.tsx
↓
5 smaller .tsx files
Das eigentliche Ziel ist eine schichtweise Struktur, in der UI, Verhalten, Domänenlogik und Datenzugriff jeweils ihren eigenen Platz haben:
ProductPage
│
├─────────────── UI
│
├─────────────── Hooks / Behavior
│
├─────────────── Domain Logic
│
└─────────────── Data Access
In der Praxis ist ein auf Funktionen ausgerichteter Ordner eine praktische Möglichkeit, dies darzustellen:
features/products/
│
├── components/
│ ├── ProductFilters.tsx
│ ├── ProductTable.tsx
│ ├── ProductActions.tsx
│ └── ProductDialog.tsx
│
├── hooks/
│ ├── useProducts.ts
│ ├── useProductFilters.ts
│ └── useProductSelection.ts
│
├── services/
│ └── productService.ts
│
├── utils/
│ ├── productMapper.ts
│ └── productValidator.ts
│
└── types.ts
Die Ordnernamen sind weniger wichtig als das, was jedes Modul beinhaltet:
ProductTableist für die Darstellung der Tabelle verantwortlich.useProductFiltersenthält das Filterverhalten.useProductsstellt die Produktdaten React zur Verfügung.productServicekommuniziert mit dem Backend.productValidatorenthält reine Validierungsregeln.
Die gewünschte Struktur ist ein einrichtungsfreier Datenfluss, bei dem jede Schicht nur von der darunterliegenden abhängt:
UI
↓
Behavior
↓
Data Access
↓
Backend
und nicht ein Web, in dem jede Angelegenheit in alle anderen eingreift:
UI
↕
State
↕
API
↕
Business Logic
↕
UI
Um sich den Aspekt des Prop-Drillings dieses Problems genauer anzusehen, lesen Sie „Cutting React Prop Drilling and God Components Down to Size“.
Schritt eins: Reine Logik aus React herausnehmen
Pure-Funktionen stellen die risikoärmste Methode zur Extraktion dar und machen sie daher zum natürlichen Ausgangspunkt. Hilfsfunktionen für den Status werden in der Regel innerhalb der Komponente definiert:
const getProductStatus = (product: Product) => {
if (!product.active) {
return "Inactive";
}
if (product.stock <= 0) {
return "Out of Stock";
}
return "Available";
};
Es hängt dort nichts von React ab: kein useState, kein useEffect, kein Redux, kein Context. Es gibt keinen Grund, dass die Komponente diese Funktionen enthält – daher sollten sie in ein eigenes Modul verschoben werden:
utils/productStatus.ts
Sobald sie dort liegen, kann die Regel direkt getestet werden, ohne etwas rendern zu müssen:
describe("getProductStatus", () => {
it("returns inactive for disabled products", () => {
expect(
getProductStatus({
active: false,
stock: 10
})
).toBe("Inactive");
});
});
Das ist ein erheblicher Vorteil bei sehr geringem Risiko: Die Geschäftsregel ist nun von der Darstellung getrennt, und ein fehlgeschlagener Test weist direkt auf die Regel statt auf eine Seite. Das Leitprinzip ist einfach: Wenn eine Logik React nicht benötigt, sollte React nicht dafür verantwortlich sein.
Zweiter Schritt: React-Verhalten in Hooks extrahieren
Dann kommen die Teile, die tatsächlich zu React gehören: Zustände und Effekte, die zusammenarbeiten. Das Laden von Produkten ist ein typisches Beispiel mit drei Zustandsvariablen und einem Effekt:
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchProducts()
.then(setProducts)
.catch(setError)
.finally(() => setLoading(false));
}, []);
Indem es wortwörtlich in einen benutzerdefinierten Hook übertragen wird, entsteht eine einzige Einheit mit einem kleinen Rückgabewert:
function useProducts() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchProducts()
.then(setProducts)
.catch(setError)
.finally(() => setLoading(false));
}, []);
return {
products,
loading,
error
};
}
Der Bildschirm verwendet es anschließend in einer Zeile:
function ProductManagement() {
const {
products,
loading,
error
} = useProducts();
// composition
}
Der Komponentencode enthält nun weniger Implementierungsdetails und liest eher wie eine Beschreibung dessen, aus was der Bildschirm besteht. Das Wortwörtliche Übertragen des Codes ist beabsichtigt: Die Extraktion sollte das Verhalten nicht ändern. Verbesserungen wie das Ansetzen von loading initial auf true (damit die erste Darstellung nicht kurzzeitig „geladen“ aussieht), das Ignorieren von Antworten nach dem Entfernen der Komponente oder die Verwendung einer Bibliothek für Serverzustände gehören in eine separate, spätere Änderung.
Nicht alles sollte zu einem Hook werden
Hier neigen Refaktorisierungen dazu, überdimensioniert zu werden. Einige Codebasen enden schließlich mit einem Hook für jede kleine Funktion:
useProductName()
useProductStatus()
useProductLabel()
useProductColor()
useProductFormatter()
Das ist keine architektonische Verbesserung. Ein benutzerdefinierter Hook rechtfertigt seine Existenz, wenn er Reacts Verhalten abdeckt: Zustand, Effekte, Context, andere Hooks, Abonnements oder Aspekte des Lebenszyklus. Wenn man lediglich einen Formatierer benötigt,
formatProductName(product)
Dann ist ein einfacher Funktionsaufruf genau das Richtige:
formatProductName(product)
Wenn man es als Hook verpackt,
useProductName(product)
kommen die Regeln für Hooks hinzu, was einen bedingten Aufruf oder den Einsatz innerhalb von Schleifen verhindert – ohne jeden Vorteil. Abstraktionen sollten die mentale Last einer Codebasis verringern, nicht erhöhen.
Bestimmen Sie, wer für jeden Zustandsteil verantwortlich ist
Der Ort, an dem der Zustand gespeichert wird, ist eine der wichtigsten Entscheidungen während einer Refaktorierung. Eine nützliche Einteilung unterscheidet zwischen Serverdaten, globaler UI-Zustandsverwaltung und lokaler UI-Zustandsverwaltung:
Application State
│
├── Server State
│ ├── Products
│ ├── Orders
│ └── Customers
│
├── Global UI State
│ ├── Authentication
│ ├── Theme
│ └── Application Preferences
│
└── Local UI State
├── Modal
├── Selected Tab
└── Temporary Form State
Keine der Extremlösungen funktioniert: Alles in Redux zu packen führt zu einem überdimensionierten globalen Store, während alles lokal gehalten wird, zu Duplikationen und Prop-Ketten. Für jeden Zustandsteil sollten Sie folgende Fragen beantworten:
- Welche Komponenten lesen ihn?
- Wie lange muss er bestehen bleiben?
- Gehört er zum Server oder zum Client?
- Muss er auch nach Navigationen erhalten bleiben?
Eine zuverlässige Heuristik: Halten Sie den Zustand so nah wie möglich bei seinen Verbrauchern – aber nicht näher, als ihre gemeinsamen Anforderungen es zulassen.
Vermeiden Sie die Schaffung eines „Gotteselterns“
Die Aufteilung einer Komponente kann ein neues Problem verursachen. Die Kinderkomponenten sind klein, doch die Elternkomponente enthält nun den gesamten Zustand und gibt ihn weiter:
<ProductPage
products={products}
filters={filters}
setFilters={setFilters}
selected={selected}
setSelected={setSelected}
permissions={permissions}
loading={loading}
error={error}
onExport={handleExport}
onDelete={handleDelete}
onEdit={handleEdit}
/>
Die Datei ist kürzer, doch die Kopplung bleibt unverändert; sie hat sich lediglich um ein Niveau nach oben verschoben. Bevor Sie auf Kontexte oder globale Speicher zurückgreifen, um diese Eigenschaften zu verbergen, fragen Sie sich, ob der Zustand tatsächlich geteilt wird oder ob die Grenzen der Komponenten an der falschen Stelle gezogen sind. Ein Kontext ist sinnvoll, wenn mehrere Nachkommenkomponenten tatsächlich denselben Wert benötigen, während ein globaler Speicher dann angebracht ist, wenn der Zustand auf gesamte Anwendungsebene gehört. Beides sollte keine Grenzen überdecken, die nicht zum jeweiligen Domänenbereich passen.
Ersetzen Sie den Komponenten schrittweise mit dem Strangler-Pattern
Die ganze Komponente auf einmal umzuschreiben, ist die riskanteste Option. Sie Stück für Stück zu ersetzen, ist weitaus sicherer. Zunächst erledigt die alte Komponente alles:
BigComponent
└── Everything
Dann wird eine Funktion, wie zum Beispiel das Filtern, in eine eigene Komponente verlegt, während der Rest unverändert bleibt:
BigComponent
├── ProductFilters
└── Everything else
In der nächsten Iteration folgt die Tabelle:
BigComponent
├── ProductFilters
├── ProductTable
└── Everything else
Schließlich besteht die Seite nur noch aus Kompositionen, und die ursprüngliche Implementierung ist verschwunden:
ProductPage
├── ProductFilters
├── ProductTable
├── ProductActions
└── ProductDialog
Das ist das Strangler-Fig-Pattern, das üblicherweise zur Migration von Backend-Systemen beschrieben wird und hier auf die Frontend-Entwicklung angewandt wird: Neue Komponenten entstehen um die alte Implementierung herum, wobei jede eine Funktion übernimmt, während der Rest der Anwendung weiterhin funktioniert. Der Vorteil ist, dass die Produktion niemals direkt von
100% Legacy
zu
100% New
In einer einzigen Veröffentlichung. Jeder Zwischenzustand ist bereit für die Bereitstellung und rückgängig machbar.
Halten Sie den öffentlichen Vertrag stabil
In größeren Organisationen wird eine solche Komponente oft von mehreren Bildschirmen mit einer bestimmten Menge an Eigenschaften verwendet:
<ProductManagement
clientId={clientId}
onSelect={handleSelect}
/>
Halten Sie diese Schnittstelle während der Refaktorierung unverändert. Dahinter können Sie sie vollständig neu organisieren:
Existing Public API
│
▼
ProductManagement
│
┌────┼────┐
▼ ▼ ▼
Hooks UI Services
Die Nutzer müssen niemals wissen, dass die Interna in Hooks, UI-Komponenten und Dienste verlagert wurden – dadurch bleibt der Einflussbereich jeder Änderung gering. Diese Disziplin ist besonders wichtig für gemeinsame Komponentenbibliotheken, Design-Systeme, Micro-Frontends, Unternehmensanwendungen sowie alles, was von mehreren Teams genutzt wird. Falls die API tatsächlich geändert werden muss, geschieht dies als separate, explizite Migration.
Lösen Sie Effekte auf, die mehrere Verantwortlichkeiten verbergen
Große Komponenten enthalten häufig Effekte, die mehrere unzusammenhängende Aufgaben ausführen:
useEffect(() => {
fetchData();
updateAnalytics();
validatePermissions();
calculateSomething();
}, [dependency]);
Das Problem liegt nicht in der Anzahl der Anweisungen, sondern darin, dass sie vermutlich vier separate Verantwortungsbereiche repräsentieren, jeder mit seinen eigenen tatsächlichen Abhängigkeiten. Teilen Sie sie auf, sodass jeder Effekt klar angibt, worauf er tatsächlich reagiert:
useEffect(() => {
fetchData();
}, [dependency]);
useEffect(() => {
updateAnalytics();
}, [analyticsDependency]);
Wenn Sie schon dabei sind, suchen Sie nach Effekten, die nur Werte berechnen. Das Speichern einer gefilterten Liste im Zustand und deren Synchronisierung über einen Effekt verursacht eine zusätzliche Renderung und führt zu veralteten Daten:
const [filteredProducts, setFilteredProducts] = useState([]);
useEffect(() => {
setFilteredProducts(
products.filter(product => product.active)
);
}, [products]);
Die Berechnung während des Renderings ist einfacher und immer konsistent:
const filteredProducts = products.filter(
product => product.active
);
Cachen oder Synchronisieren Sie nur, wenn es einen echten Grund gibt – beispielsweise bei einer aufwändigen Berechnung, für die useMemo das geeignete Mittel ist und kein Effekt.
Trennen Sie den Serverzustand vom UI-Zustand
Auf größeren Bildschirmen lohnt es sich schnell, diese beiden Kategorien voneinander zu trennen:
ProductPage
│
┌───────────┴───────────┐
│ │
Server State UI State
│ │
┌─────┴─────┐ ┌──────┴──────┐
│ │ │ │
products loading filter modal
error status selected tab
Die Produktliste und die Frage, ob ein Dialog geöffnet ist, sind grundlegend unterschiedliche Arten von Daten. Der Backend-Teil verwaltet die Produkte; der Browser verwaltet den Dialog. Serverdaten benötigen Caching, Neuabruf, Laden sowie Fehlerbehandlung, während der UI-Zustand vorübergehend und lokal ist. Das Vermischen beider in denselben useState-Aufrufen erschwert das Verständnis der Komponenten. Wie sich dies mit speziellen Tools darstellt, wird in React Query und Redux: Neubetrachtung des Serverzustands in großen Anwendungen erläutert.
Ein Refactoring-Workflow, keine Neuimplementierung
Ein Produktiv-Refactoring ist ein Kreislauf:
Identify
↓
Understand
↓
Protect
↓
Extract
↓
Test
↓
Validate
↓
Deploy
↓
Measure
↓
Repeat
Es handelt sich dabei nicht um Folgendes:
Delete component
↓
Rewrite everything
↓
Hope production works
Genau dieser Unterschied unterscheidet das Refactoring eines laufenden Systems von der Gründung eines neuen Projekts von Grund auf.
Wie der resultierende Datenfluss aussieht
Durch getrennte Verantwortlichkeiten durchläuft eine Benutzerinteraktion definierte Schichten und kehrt anschließend zurück:
User
│
▼
ProductPage
│
▼
useProductFilters
│
▼
useProducts
│
▼
ProductService
│
▼
API
│
▼
Backend
│
▼
ProductService
│
▼
useProducts
│
▼
ProductTable
│
▼
Updated UI
Kurz gesagt: Es handelt sich um eine einfache Abhängigkeitslinie:
Presentation
↓
Behavior
↓
Data Access
↓
Backend
Die Tabelle sollte nicht wissen, wie die Authentifizierung funktioniert, der API-Service sollte nicht wissen, wie die Tabelle dargestellt wird, und eine Geschäftsregel sollte sich nicht darum kümmern, ob die Benutzeroberfläche ein Modalfenster oder einen Ausklappmechanismus verwendet. Das ist die Trennung von Aufgaben mit einem konkreten Zweck: Jede Änderung bleibt in einer Schicht.
Achten Sie auf duplizierte API-Anfragen
Diese Regression tritt so häufig auf, dass sie eine eigene Warnung verdient. Vor der Refaktorierung lädt ein Komponente die Daten herunter:
ProductPage
│
└── fetchProducts()
Nach der Extraktion rufen sowohl die Seite als auch ein Kindkomponente denselben Daten-Hook auf:
ProductPage
│
├── useProducts()
│
└── ProductTable
│
└── useProducts()
Der Code sieht ordentlicher aus, doch ein einfacher fetch-in-effect-Hook wird nun zweimal ausgeführt, was die Anfragen verdoppelt und möglicherweise unterschiedliche Zustände der beiden Komponenten zeigt. Immer wenn Sie die Logik zum Abrufen von Daten verschieben, fragen Sie nach, wer für diesen Serverzustand verantwortlich ist. Es sollte eine einzige Quelle der Wahrheit geben. Wenn mehrere Komponenten dieselben entfernten Daten benötigen, sollten diese entweder einmal abgerufen und weitergegeben werden oder es sollte eine cachebasierte Schicht für den Serverzustand verwendet werden, die doppelte Anfragen vermeidet, anstatt jedem Verbraucher die Möglichkeit zu geben, unabhängig zu laden.
Tests sind das Sicherheitsnetz
Vor der Änderung einer kritischen Komponente listen Sie die Funktionalitäten auf, von denen das Geschäft abhängt:
Product Management
│
├── Filter products
├── Select products
├── Edit product
├── Delete product
├── Export products
└── Permission handling
Dann schützen Sie diese auf der richtigen Ebene:
- Unit-Tests eignen sich zur Überprüfung von Validierungen, Transformationen, Geschäftsregeln, Formatierungen und Zuordnungen.
- Komponententests eignen sich für das Rendering, die Interaktionen sowie Zustände beim Laden und bei Fehlern.
Eine Refaktorierung erfordert keine 100%-Abdeckung. Es genügt, Vertrauen in das wichtige Verhalten zu haben. Tests, die vor dem Start basierend auf dem aktuellen Verhalten erstellt werden, sind am wertvollsten, denn sie belegen, dass sich nichts geändert hat.
Kleine Pull-Requests gehören zum Design
Die Kosten für die Überprüfung und den Rollback steigen mit der Größe einer Änderung. Vergleichen Sie eine solche Reihe:
Extract ProductFilters
folgt auf
Extract ProductTable
und dann
Extract product-fetching behavior
mit einer einzigen Änderung mit dem Titel
Refactor ProductManagement
die betroffen ist
4,000 lines changed
Kleine Pull Requests sind leichter zu überprüfen und rückgängig zu machen, verringern das Risiko von Regressionen, hinterlassen eine lesbare Git-Geschichte, vereinfachen das Debuggen und reduzieren Merge-Konflikte. Für einen Tech-Lead ist dies weniger eine Präferenz bezüglich der Codequalität als vielmehr ein Weg, damit das Team parallel weiterarbeiten kann.
Refactoring von anderen Änderungen trennen
Wehren Sie sich dagegen, ein Refactoring mit allen anderen Punkten auf der Wunschliste zusammenzufassen:
React upgrade
+
State migration
+
API changes
+
UI redesign
+
CSS migration
+
Component refactoring
Falls nach einer solchen Veröffentlichung die Produktion ausfällt, ist es schwierig, die Ursache zu finden, da jede der sechs Änderungen dafür verantwortlich sein könnte. Ordnen Sie sie stattdessen nacheinander an:
Refactor
↓
Stabilize
↓
Measure
↓
Optimize
Eine bedeutende Änderung auf einmal sorgt dafür, dass jede Regression zugeordnet werden kann.
Feature-Flags für hochriskante Ersetzungen
Für kritische Arbeitsabläufe bietet ein Feature-Flag eine weitere Sicherheitsebene, indem es beiden Implementierungen die gleichzeitige Existenz ermöglicht:
return useNewProductExperience
? <NewProductManagement />
: <LegacyProductManagement />;
Dann können Sie die Exponierung schrittweise erweitern, während Sie die Fehlerraten sowie die Geschäftsmetriken überwachen:
1%
↓
5%
↓
25%
↓
50%
↓
100%
Das ist besonders nützlich für Seiten mit hohem Traffic, prozesskritische Abläufe, Unternehmensanwendungen, große Micro-Frontends sowie risikoreiche architektonische Änderungen. Behandeln Sie das Flag als vorübergehend: Sobald sich der neue Weg bewährt hat, löschen Sie die alte Implementierung sowie das Flag – andernfalls müssen Sie beides weiterhin warten.
Vor und nach
Der Ausgangspunkt, zusammengefasst, kombiniert Datenverarbeitung, Filtern, Auswahl, Export und Dialoge in einer einzigen Funktion:
function ProductManagement() {
const [products, setProducts] = useState([]);
const [filter, setFilter] = useState("");
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const [showDialog, setShowDialog] = useState(false);
useEffect(() => {
// Fetch products
}, []);
const filteredProducts = products.filter(...);
const handleSelect = (...) => {
// Selection logic
};
const handleExport = (...) => {
// Export logic
};
return (
<>
{/* hundreds of lines of JSX */}
</>
);
}
Keines davon ist unbedingt falsch. Das Problem besteht darin, dass eine Komponente für zu viele Aufgaben verantwortlich ist. Nach der Refaktorierung verbindet die Seite Hooks mit den präsentativen Komponenten:
function ProductManagement() {
const {
products,
loading
} = useProducts();
const {
filter,
setFilter,
filteredProducts
} = useProductFilters(products);
const {
selected,
handleSelect
} = useProductSelection();
return (
<ProductPage>
<ProductFilters
value={filter}
onChange={setFilter}
/>
<ProductTable
products={filteredProducts}
selected={selected}
onSelect={handleSelect}
loading={loading}
/>
<ProductActions
products={selected}
/>
</ProductPage>
);
}
Die Seite beschreibt nun die Komposition, welche die gesündeste Struktur ist, die ein komponentenbasierter Seitenanteil haben kann. Jeder Hook sowie jedes Kindkomponente können unabhängig voneinander gelesen, getestet und verändert werden.
Kleiner bedeutet nicht schneller
Es ist verlockend anzunehmen, dass die Umwandlung
1 large component
in
10 smaller components
die Leistung verbessert. Das geschieht von selbst nicht, und es kann die Situation sogar verschlechtern, indem zusätzliche Darstellungen, wiederholte Berechnungen, störende Kontextaktualisierungen, doppelte Anfragen, überflüssige useMemo- und useCallback-Funktionen sowie Props, deren Identität bei jeder Darstellung ändert, entstehen. Architektur und Leistung hängen zusammen, sind aber unterschiedliche Aspekte. Messen Sie vor der Optimierung und nutzen Sie Profilierung sowie Produkt-Telemetrie, um zu überprüfen, ob tatsächlich etwas verbessert wurde.
Bewerten Sie den Erfolg anhand der Kosten des nächsten Änderungsversuchs
Eine abnehmende Anzahl von Zeilen ist ein angenehmer Nebeneffekt:
1,200 lines → 300 lines
Aber das ist nicht das Ziel. Vergleichen Sie den Komponenten vor und nach in Bezug auf Aspekte wie:
- kognitive Komplexität und Kopplung, die sinken sollten
- Testbarkeit, die steigen sollte
- doppelte API-Aufrufe sowie die Anzahl der Fehler in der Produktion
- Render-Zeiten in Millisekunden
- Rückfallrate nach Veröffentlichungen
- der für eine typische Änderung erforderliche Aufwand
Die Richtungen, auf die Sie abzielen sollten:
Complexity ↓
Coupling ↓
Regression risk ↓
Testability ↑
Maintainability ↑
Developer speed ↑
Das wertvollste Ergebnis ist eine geringere Kosten für Änderungen, nicht weniger Zeilencode.
Wann man nicht refaktorieren sollte
Nicht jedes große Komponenten verdient diese Anstrengung. Zu den schlechten Gründen gehören: Die Datei ist lang, jemand bevorzugt kleine Dateien, eine neue Architektur ist im Trend, man möchte eine weitere Bibliothek einfügen oder der Code entspricht nicht dem persönlichen Geschmack. Wenn die Komponente stabil, gut getestet, selten geändert, risikofrei und isoliert ist, kann eine umfassende Refaktorierung mehr Risiken mit sich bringen, als sie beseitigt. Die Entscheidung hängt von einem Kompromiss ab:
Expected future benefit
│
▼
Is it worth the
refactoring risk?
│
Yes / No
Das ist ein ingenieurtechnisches Urteil bezüglich erwarteter zukünftiger Änderungen, keine Formatierungspräferenz.
Das Verständnis des bestehenden Verhaltens ist der schwierige Teil
Die Erstellung der neuen Implementierung ist selten der schwierige Teil. Die Herausforderung liegt darin, das aktuelle Verhalten gut genug zu verstehen, um das beizubehalten, worauf die Nutzer angewiesen sind. Produktionskomponenten enthalten Kenntnisse, die sich nicht aus dem Lesen selbst erschließen: ungeschriebene Produktannahmen, Zugriffsregeln, Anbindungen an ältere Systeme, ungewöhnliche Backend-Antworten, die sie tolerieren, Ereignisse zur Überwachung, auf die andere Systeme angewiesen sind, Workarounds für bestimmte Browser, Aufrufer, an die sich niemand mehr erinnert, Patches aus früheren Vorfällen sowie Erwartungen anderer Teams. Gerade dieses verborgene Wissen macht vollständige Neuschreibungen weitaus riskanter als schrittweise Aufteilung. Die Prioritätenreihenfolge, die man im Hinterkopf behalten sollte: Verhalten beibehalten, anschließend Struktur verbessern und zuletzt optimieren.
Checkliste für Refactoring in der Produktion
Vor dem Start:
- Erfassen Sie alle Nutzer der Komponente.
- Dokumentieren Sie ihr aktuelles Verhalten und legen Sie ihre Aufgaben fest.
Beim Refactoring:
- Den öffentlichen Vertrag stabil halten.
- Zuerst reine Funktionen extrahieren, anschließend das React-Behavior.
- Den Zustand in der Nähe seiner Verbraucher halten und Abstraktionen vermeiden, die sich nicht lohnen.
- Nicht verwandte Änderungen auslassen, auf doppelte Anfragen achten und PRs klein halten.
Vor dem Merge:
- Typüberprüfungen, Unit-, Komponenten- und End-to-End-Tests sowie der Build müssen alle bestehen.
- Keine doppelten Anfragen und keine unerwarteten Rendering-Rückfälle.
Nach dem Deploy:
- Fehler, API-Verkehr und Leistung überwachen sowie mit der Baseline vergleichen.
- Auf kritische Benutzerarbeitsabläufe achten.
- Entfernen Sie veralteten Code, sobald der neue Pfad Vertrauen gewonnen hat.
Zusammenfassung
Große React-Komponenten sind nicht das Problem; unkontrollierte Komplexität schon. Die fehlerhafte Strategie sieht so aus:
Big Component
↓
Rewrite Everything
Die erfolgreiche Strategie besteht darin, geduldig Schritt für Schritt vorzugehen, bis schließlich eine einfachere Architektur entsteht:
Big Component
↓
Understand
↓
Protect
↓
Extract
↓
Test
↓
Measure
↓
Repeat
↓
Simpler Architecture
Das Ziel ist es nicht, mehr Dateien zu haben, sondern sicherere Änderungen: Releases, die niemanden überraschen, sowie eine Codebasis, die der nächste Entwickler in sechs Monaten ohne Angst ändern kann.