Frontend-Schulden als Änderungskosten: Die Diagnose einer wachsenden React-Codebasis
Erlernen Sie, die sich gegenseitig verstärkenden Entscheidungen zu erkennen, die es schwierig machen, große React-, Redux-, TypeScript- und Next.js-Anwendungen zu ändern, sowie wie Sie diese Schulden schrittweise abbauen können.
Quellen: Frontend Technical Debt: The Problems Nobody Notices Until the React App Becomes Huge
Große React-Anwendungen werden fast nie aufgrund eines einzigen katastrophalen Commit schwer zu warten. Sie verschlechtern sich durch eine lange Reihe kleiner, einzeln sinnvoller Entscheidungen: ein zusätzliches useEffect, ein Teil des Redux-Zustands, der nicht global sein musste, ein „temporärer“ 500-Zeilen-Component, ein gemeinsames Widget, das ständig mehr Props erhält, eine utils-Datei, die alles aufnimmt, was keinen offensichtlichen Platz hat, ein API-Typ, der in drei Module kopiert wurde, sowie eine Styling-Regel, an der sich niemand traut, etwas zu ändern. Keine dieser Entscheidungen erscheint zum Zeitpunkt ihrer Getroffenheit falsch. Das Problem beginnt, wenn sich Hunderte davon anhäufen und miteinander interagieren.
Diese Ansammlung ist das, was wir unter technischem Schuldenberg im Frontend verstehen, und die Kernidee dieses Leitfadens ist einfach: Die größten Probleme im Frontend werden selten durch einen einzigen schlechten Codeausschnitt verursacht, sondern durch kleine Entscheidungen, die sich summieren. Unten finden Sie die häufigsten Formen, in denen sich dieser Schuldenberg in Codebasen von React, Redux, TypeScript und Next.js zeigt, warum jede davon kostspielig wird, sowie eine praktische Methode, ihn zu verringern, ohne die Produktentwicklung für eine Neuimplementierung zum Stillstand zu bringen.
Wie sich ein sauberer Repository verunreinigt
Ein neues Projekt beginnt in der Regel mit einer Struktur, die jeder auf den ersten Blick verstehen kann:
src/
components/
pages/
hooks/
services/
store/
Ein Entwickler, der den Repository noch nie gesehen hat, kann erraten, wohin ein neuer Hook oder eine neue Dienstleistung gehören sollte. Dann wächst das Produkt, das Team und die Liste der Funktionen. Einige Jahre später sieht derselbe Aufbau oft so aus:
src/
components/
Button/
Modal/
Table/
UserTable/
UserTableNew/
CommonTable/
GenericTable/
SharedTable/
...
hooks/
useUser.ts
useUsers.ts
useUserData.ts
useFetchUser.ts
useUserDetails.ts
...
utils/
helpers.ts
common.ts
misc.ts
formatter.ts
newHelpers.ts
oldHelpers.ts
store/
user/
users/
userData/
userProfile/
...
Achten Sie auf das Muster: mehrere Tabellen mit überschneidenden Zwecken, fünf Hooks, die alle „einen Benutzer“ auf leicht unterschiedliche Weise abrufen, Hilfdateien, die nur durch die Wörter „neu“ und „alt“ unterschieden werden, sowie vier Speicherverzeichnisse für das, was eigentlich ein einziger Domainbereich ist. Niemand hat das so entworfen – es hat sich Schritt für Schritt entwickelt, mit jedem Pull Request, und genau deshalb ist diese Art von Schulden gefährlich: Es gibt nie einen klaren Moment, in dem offensichtlich eine Korrektur notwendig ist.
Der Abkürzungsweg, der stillschweigend dauerhaft wird
Die bekannteste Quelle für solche Schulden ist die verschobene Aufräumarbeit. Eine Funktion muss noch heute veröffentlicht werden; eine ordentliche Umsetzung würde zwei Tage dauern, die schnelle Version hingegen drei Stunden. Die Wahl der dreistündigen Version ist nicht automatisch eine schlechte ingenieurstechnische Entscheidung – manchmal ist sie gerade die richtige geschäftliche Entscheidung.
Das Risiko entsteht, wenn ein Abkürzungsweg länger existiert als der ursprüngliche Grund dafür. Temporärer Code neigt stark dazu, zu Produktiv-Infrastruktur zu werden, und die Entscheidungen, die ihn erstellen, klingen in dem Moment meist unbedenklich:
„Legen wir das einfach in den bestehenden Komponenten ein.“ „Wir refaktorisieren das nach der Veröffentlichung.“ „Vorerst behalten wir es in Redux.“ „Wir können es später verallgemeinern.“ „Andere Funktionen werden das nie benötigen.“
Ein halbes Jahr später ist der ursprüngliche Kontext verschwunden. Niemand erinnert sich mehr daran, warum der Code so geschrieben wurde; die Person, die ihn geschrieben hat, könnte in ein anderes Team gewechselt sein, die Anforderung, der er diente, hat sich geändert, und drei weitere Funktionen verlassen sich nun auf sein Verhalten. Um einen dreistündigen Abkürzungsweg rückgängig zu machen, können dann drei Wochen vergehen.
Wenn eine Komponente eine ganze Funktion steuert
Der „Fettkomponenten“-Effekt ist eines der am leichtesten erkennbaren Symptome. Er beginnt in der Regel mit einer scheinbar vernünftigen Auflistung von Aufgaben, die innerhalb einer einzigen Funktion dargestellt werden:
function Checkout() {
// fetch products
// validate coupon
// calculate totals
// manage address
// handle payment
// track analytics
// show modals
// handle errors
// render UI
}
Jede neue Anforderung landet am selben Ort, weil dort bereits der zugehörige Code vorhanden ist. Letztendlich übernimmt das Komponente folgende Aufgaben:
- Kommunikation mit der API
- Business-Regeln und Validierung
- Zustandsverwaltung
- Analysen und Berechtigungsprüfungen
- Navigation und Fehlerbehandlung
- Styling und Darstellung
React trägt hier eigentlich keine Schuld. Das eigentliche Problem ist, dass zu viele Verantwortlichkeiten über die Grenzen der Komponente hinausgehen. Eine Komponente eignet sich gut zur Darstellung sowie zur Verwaltung des UI-Zustands, der zu diesem Teil der Benutzeroberfläche gehört; sie sollte nicht zum Container für alle Aspekte werden, die eine Funktion betrifft. Preisregeln, Gutschein-Validierung und Analysen können in normalen Modulen oder benutzerdefinierten Hooks untergebracht werden, die ohne jegliche Darstellung getestet werden können.
Wiederverwendbare Komponenten, die mehr kosten als sie einsparen
Zu Beginn einer Karriere erscheint Wiederverwendbarkeit eindeutig vorteilhaft. Mit Erfahrung wird klar, dass jede Abstraktion ihren Preis hat. Betrachten Sie eine Tabelle, die als „konfigurierbar“ erstellt wurde:
<DataTable
data={data}
columns={columns}
loading={loading}
selectable
searchable
sortable
pagination
serverPagination
expandable
virtualized
stickyHeader
renderEmptyState={...}
renderLoadingState={...}
onRowClick={...}
/>
Im technischen Sinne ist es wiederverwendbar. Ob es wartbar ist, ist eine andere Frage – und die ehrliche Antwort lautet „es hängt davon ab“. Jedes boolesche Flag verdoppelt die Anzahl der Kombinationen, die das Komponente unterstützen muss, und jeder neue Nutzer, der eine leichte Abwandlung benötigt, fügt statt einer weiteren Komponente einfach ein weiteres Attribut hinzu.
Ein nützlicherer Test als „Kann das wiederverwendet werden?“ ist die Frage, ob die Abstraktion den Aufbau der nächsten Funktion erleichtert. Eine Komponente mit dreißig Attributen ist oft überhaupt keine wiederverwendbare Architektur – es handelt sich dabei um mehrere unterschiedliche Komponenten, die hinter einer einzigen API zusammengefasst sind. Die Komposition, bei der Nutzer kleinere Teile zusammensetzen, ist oft der bessere Ausweg, wie in „Fixing React Prop Overload with Composition and Slots“ erläutert.
Wo die utils-Ordner Ihren Domainnamen verstecken
Fast jede größere Frontend-Anwendung entwickelt einen utils-Ordner, und anfangs ist das unbedenklich. Im Laufe der Zeit sieht es meist so aus:
utils/
formatDate.ts
formatCurrency.ts
userHelpers.ts
permissions.ts
productHelpers.ts
validation.ts
apiHelpers.ts
stringUtils.ts
common.ts
helpers.ts
Sobald das Verzeichnis diesen Zustand erreicht, kann niemand mehr erkennen, wohin die neue Logik gehört. Deshalb wird der nächste Hilfscode in common.ts aufgenommen, und auch der danach. Der Code ist nach Dateitypen organisiert, doch das Geschäftsfeld bleibt unsichtbar. Berechtigungsprüfungen, Produktregeln sowie API-Verarbeitungen liegen neben der Zeichenformatierung.
Die bessere Frage lautet nicht „In welche Hilfsdatei soll das gehören?“, sondern „Welche Geschäftsfunktion besitzt diese Logik?“. Eine auf Funktionen ausgerichtete Struktur macht diese Antwort klar:
features/
checkout/
components/
hooks/
services/
types/
utils/
products/
components/
hooks/
services/
types/
payments/
components/
hooks/
services/
types/
Das ist keine Anweisung, überall Funktionsordner einzuführen. Das zugrundeliegende Prinzip ist wichtiger als die Verzeichnisnamen: Der Code sollte nach Eigentum und Verantwortung gruppiert werden, nicht nur nach der Art des Dateityps.
Redux als Allzweck-Store
Redux ist ein wirklich nützliches Werkzeug, aber alles im globalen State zu speichern, ist einer der schnellsten Wege, Frontend-Schulden aufzubauen. Der Store beginnt in der Regel mit Slices, die eindeutig global sein sollten:
auth
user
permissions
cart
products
checkout
notifications
Dann nimmt es Elemente auf, die sich tatsächlich auf einen Bildschirm oder eine Interaktion beziehen:
modal
filters
selectedTab
searchText
formState
temporaryUIState
Irgendwann verhält sich der Store wie eine anwendungsweite Datenbank für alles, was zwei Komponenten möglicherweise lesen möchten. Technisch gesehen kann fast jeder Wert in Redux gespeichert werden, daher ist die Frage „Kann das?“ die falsche. Die richtige Frage ist, ob der Zustand tatsächlich global sein muss. Die Einteilung des Zustands in drei Kategorien hilft dabei.
UI-Zustand, der lokal bleiben sollte
Werte wie diese gehören in der Regel neben der Komponente, die sie verwendet, in useState oder useReducer:
isModalOpen
activeTab
inputValue
hoveredItem
expandedRow
Durch das Hochladen in einen globalen Speicher wird jede geöffnete Modalliste oder jeder über die Maus angezeigte Zeileneintrag zu einer Belastung für die gesamte Anwendung und fügt unnötige Verzögerungen bei einfachen Interaktionen hinzu.
Serverzustände, die eine Caching-Strategie erfordern
Daten, die beim Backend liegen, haben unterschiedliche Anforderungen:
products
orders
users
invoices
Hier sind die schwierigen Probleme das Caching, die Deduplizierung, das Neuladen sowie die Invalidation. Dafür ist ein bewusster Ansatz bei der Datenerfassung erforderlich – sei es eine spezielle Server-Zustandsbibliothek oder ein äquivalentes Muster – anstatt handgeschriebener Reducer, die Antworten in den Store kopieren. Die Abwägungen werden ausführlicher in Rethinking Server State in Large Apps with React Query and Redux erörtert.
Zustand, der tatsächlich auf die gesamte Anwendung ausgerichtet ist
Einige Probleme betreffen tatsächlich das gesamte Produkt:
authentication
permissions
application-wide preferences
cross-feature workflows
Diese sind vernünftige Kandidaten für einen globalen Store. Die Lektion lautet nicht „vermeide Redux“; vielmehr ist allein die Verfügbarkeit kein Grund, den Zustand global zu machen.
TypeScript-Debt: Typen, die keine Bedeutung mehr haben
TypeScript ist eines der wertvollsten Werkzeuge für große Frontends, doch es entwirft keine Codebasis für einen selbst. Es ist durchaus möglich, dieses Muster in einer Anwendung überall einzubauen und den Projektcode dennoch als typisiert zu bezeichnen:
const data: any = response.data;
Jeder any deaktiviert die Überprüfung genau an dem Punkt, an dem die Daten in die Anwendung eintreten, sodass der Compiler Sie nicht warnen kann, wenn sich die Struktur der API ändert. Ein weiterer häufiger Fehler ist die unabhängige Deklaration desselben Domäntyps in mehreren Dateien:
type User = {
id: string;
name: string;
}
Multipliziert man das mit einigen Teams, erhält man eine Reihe von fast identischen Versionen:
User
UserType
UserModel
UserResponse
UserDTO
UserData
Entwickler investieren dann mehr Mühe darin, zu entscheiden, welchen Typ sie importieren sollen, anstatt vom Typensystem Vorteile zu ziehen. Das Ziel, „alles soll einen Typ haben“, verfehlt den Kern der Sache. Ziel ist vielmehr, dass die wichtigen Kontrakte im System – wie API-Antworten, Domänenentitäten und Komponenteninterfaces – eindeutig sind und nur einmal definiert werden. Gutes TypeScript erschwert die Darstellung ungültiger Zustände; schlechtes TypeScript erzeugt lediglich mehr Dateien. Eine nützliche Unterscheidung besteht darin, einen Transporttyp für das zu verwenden, was die API zurückgibt, und einen Domäntyp für das, womit die Benutzeroberfläche arbeitet – beides wird an einer Stelle umgewandelt, anstatt sechs lose miteinander verbundene Varianten zu verwenden.
Wenn useEffect zur Standardlösung wird
Effects sind eine weitere häufige Quelle für langsam wachsende Probleme. Es beginnt mit einem einzigen Effect, der auf einen Wert reagiert:
useEffect(() => {
// do something
}, [something]);
Dann sorgt ein zweiter Effekt dafür, dass zwei Werte synchron bleiben:
useEffect(() => {
// synchronize something
}, [a, b]);
Dann existiert eine dritte hauptsächlich dazu, das Verhalten einer früheren zu korrigieren:
useEffect(() => {
// fix previous effect
}, [data]);
Der Komponente entwickelt sich zu einer Kette von Effekten, die weitere Effekte auslösen, was schwer nachvollziehbar ist und zu zusätzlichen Renderungen sowie Problemen bei der Reihenfolge führt. In dieser Phase hängt das Gelingen bei der Arbeit mit React weniger vom Wissen über die API ab und mehr von einem klaren Verständnis dafür, wie Daten durch die Komponente fließen.
Ein typisches Beispiel ist das Speichern von abgeleiteten Daten im State und deren Synchronisierung mit einem Effekt:
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Dies wird einmal mit einem veralteten leeren String gerendert, danach wird der Effekt ausgeführt und anschließend erneut gerendert. Der Wert kann stattdessen einfach während des Renderns berechnet werden:
const fullName = `${firstName} ${lastName}`;
Es scheint sich um einen unbedeutenden Unterschied zu handeln, doch in einer Anwendung summieren sich diese kleinen Entscheidungen auf. Effects dienen dazu, mit Systemen außerhalb von React zu synchronisieren – wie beispielsweise Abonnements, Timern oder dem DOM – und nicht dazu, einen Zustandswert aus einem anderen abzuleiten. Eine ausführlichere Erklärung finden Sie unter stop syncing state with useEffect.
Die Server/Client-Grenze in Next.js
Das moderne Next.js fügt architektonischen Entscheidungen eine neue Dimension hinzu. Neben der Frage, welcher Komponente etwas darstellt, muss man auch entscheiden, wo der Code ausgeführt wird: auf dem Server, auf dem Client oder auf beiden. Diese Grenze selbst ist eine architektonische Entscheidung.
Ein häufiger Fehler besteht darin, einen großen Teil des Codebaums in Client Components umzuwandeln, nur weil ein kleines Element Interaktivität benötigt. Die entsprechende Anweisung wird an die Spitze der Seite-Datei gesetzt:
"use client";
Und die gesamte Seite wird nun als Client-Side-JavaScript an den Browser gesendet:
export default function ProductPage() {
// entire page becomes client-side
}
Alles nur, weil eine einzige Schaltfläche einen Teil des lokalen Zustands benötigt:
const [isOpen, setIsOpen] = useState(false);
Das ist zwar praktisch, doch diese Bequemlichkeit an einer Stelle hat ihren Preis an einer anderen: Mehr JavaScript wird an den Browser gesendet, und das Datenerfassen, das ursprünglich auf dem Server stattfinden konnte, muss nun am Client erfolgen. Im App Router wird alles, was aus einer "use client"-Datei importiert wird, Teil des Client-Bundles – daher bestimmt die Platzierung der Direktive weitaus mehr als nur das eine Komponente, das interaktiv gemacht werden soll.
Es hilft, in Schichten zu denken und die Interaktivität so tief wie möglich im Codebaum zu platzieren:
Server
↓
Data fetching
↓
Feature boundary
↓
Interactive client component
↓
Small local state
anstatt des flachen Shortcuts:
Everything → Client Component
In der Praxis bedeutet das in der Regel, dass die interaktive Schaltfläche in ein eigenes kleines Client-Komponente ausgegliedert wird, während die Seite, das Laden der Daten sowie der größte Teil der Markup-Struktur auf dem Server verbleiben.
Styling-Schulden sind weiterhin Schulden
Teams betrachten technische Schulden oft als ein Problem von JavaScript und TypeScript. Auch beim Styling entstehen genauso zuverlässig Schulden – insbesondere in langfristig genutzten Produkten, die mehrere Ansätze gleichzeitig kombinieren: CSS-in-JS und styled-components, CSS-Module, Utility-Klassen, alte globale Stylesheets sowie mehr als ein Design-System. Früher oder später enthält die Komponentenbibliothek etwas in der Art:
Button
ButtonV2
PrimaryButton
NewButton
StyledButton
CommonButton
LegacyButton
Und niemand kann sagen, welcher Button der richtige ist. Die Styling-Technologie ist selten die Ursache. Der eigentliche Mangel liegt in einem Fehlen von Verantwortung für das Design-System sowie klaren Grenzen der Komponenten. Ein ausgereiftes Frontend hat Antworten auf Fragen wie:
- Welche Komponenten gehören zum öffentlichen, gemeinsamen Set?
- Welche existieren nur für eine bestimmte Funktion?
- Welches Team ist für jede davon verantwortlich?
- Wie wird auf legale Änderungen reagiert?
- Wie werden breaking changes eingeführt?
- Was erfüllt die Voraussetzungen für eine Aufnahme ins Design-System?
- Was muss lokal bleiben und niemals in das gemeinsame Set aufgenommen werden?
Wenn Duplikation besser ist als Abstraktion
Diese Lektion fällt erfahrenen Entwicklern oft am schwersten zu akzeptieren. Angenommen, es gibt zwei Komponenten, die derzeit zu etwa 90 Prozent gleich aussehen:
ComponentA
ComponentB
Der Instinkt ist es, sie zu einem zu verschmelzen:
<UniversalComponent />
Doch Duplikation ist manchmal die günstigere Option, weil man noch nicht weiß, ob sich beide weiterhin in dieselbe Richtung entwickeln werden. Wenn sie auseinanderdriften, füllt sich der zusammengeführte Komponent mit Bedingungen, und jede Änderung an einem Aufrufer birgt das Risiko, den anderen zu stören. Ein wenig Duplikation ist vorteilhaft; eine vorzeitige Abstraktion kann eine Kopplung schaffen, die jahrelang anhält.
Eine praktische Standardlösung: Schreiben Sie es zweimal, untersuchen Sie das Muster und erstellen Sie eine Abstraktion nur dann, wenn ein dritter Anwendungsfall die Struktur bestätigt. Es handelt sich dabei nicht um ein universelles Gesetz, aber es ist eine sinnvolle Ausgangsregel.
Abhängigkeiten, die nicht mehr gerechtfertigt sind
Große Frontends sammeln schnell Abhängigkeiten an. Eine fünf Jahre alte Codebasis kann leicht Bibliotheken für folgende Bereiche enthalten:
- Zustand, Formulare und Validierung
- Daten, Diagramme und Animationen
- UI-Kit-Bibliotheken und Styling
- Datenabruf
- Testing
Einige werden kaum noch verwendet, während andere durch Browserfunktionen oder in das Framework integrierte Fähigkeiten ersetzt wurden. Jeden Einzelnen von ihnen zu entfernen erscheint riskant, daher bleiben sie erhalten, und das Manifest verwandelt sich allmählich in ein Architekturdokument, das niemand lesen möchte:
package.json
Abhängigkeitshygiene bedeutet nicht nur das Anwenden von Sicherheitspatches. Sie beinhaltet auch die regelmäßige Prüfung, ob jede Abhängigkeit angesichts der damit verbundenen Wartungs-, Upgrade- und Verpackungskosten weiterhin notwendig ist.
Der wahre Preis: langsamere, riskantere Änderungen
Schulden sind kein Problem nur deshalb, weil Ingenieure unübersichtlichen Code als unangenehm empfinden. Sie sind ein Problem, weil sie die wirtschaftlichen Aspekte der Softwareveröffentlichung verändern. In einer gesunden Anwendung fließt eine Anfrage durch einen kurzen Prozessablauf:
Feature request
↓
Small change
↓
Tests
↓
Deploy
In einem stark verschuldeten Land wird dieselbe Anfrage zu einer Untersuchung:
Feature request
↓
Find where this behavior lives
↓
Understand 5-year-old code
↓
Check Redux dependencies
↓
Check shared component
↓
Check side effects
↓
Check styling overrides
↓
Change code
↓
Something else breaks
↓
Fix regression
↓
Add another workaround
Jeder Schritt in dieser zweiten Kette ist Zeit, die mit dem Verstehen statt mit dem Aufbauen verbracht wird, und die letzten beiden Schritte sind entscheidend: Eine Änderung stört etwas Unverwandtes, und die Korrektur führt zu einer weiteren Workaround-Lösung. Das ist der Codebase, der Zinsen zahlt – und das ist im Grunde das, was technischer Schuldenaufbau bedeutet.
Warum „es funktioniert immer noch“ ein irreführendes Signal ist
Eine Anwendung kann trotz enormer Schulden fehlerfrei laufen. Die Seiten laden, Benutzer melden sich an, Bestellungen werden bearbeitet, das Dashboard wird angezeigt und der CI-Status ist grün – daher geht jeder davon aus, dass der Code in Ordnung ist. Dann kommt eine neue Funktion, und eine scheinbar einfache Änderung dauert zwei Wochen. In diesem Moment wird die Schuldenlast sichtbar.
„Funktioniert die Anwendung?“ ist daher eine schwache Überprüfung des Zustands. Eine weitaus nützlichere Frage, insbesondere wenn Systeme wachsen, lautet: Wie teuer ist es, diese Anwendung sicher zu ändern?
Schulden abbauen ohne Neuentwicklung
Die verlockende Reaktion auf einen unübersichtlichen Codebasis ist ein vollständiger Neuaufbau. Das typische Ergebnis nach sechs Monaten ist vorhersehbar:
- die alte Anwendung läuft weiterhin in der Produktion
- der Ersatz ist nur teilweise fertiggestellt
- zwei Systeme müssen nun gewartet werden
- das Team ist erschöpft
- die Produktentwicklung steht still
Schrittweise architektonische Verbesserungen funktionieren fast immer besser. Dabei geht es zunächst darum, herauszufinden, wo die eigentlichen Probleme liegen.
Die kostspieligen Bereiche ermitteln
Widerstehen Sie dem Drang, alles zu refaktorisieren. Identifizieren Sie stattdessen die Teile des Produkts, bei denen die Entwickler immer wieder Schwierigkeiten haben, zum Beispiel:
Checkout
User Management
Search
Dashboard
Reporting
Für jeden Kandidaten sollten Sie nach diesen Anzeichen suchen:
- Bugs, die immer wieder im selben Bereich auftauchen
- Dateien, an denen die Leute Bedenken haben, sie zu bearbeiten
- Komponenten, die in fast jedem Pull Request vorkommen
- Bereiche des Produkts, die neue Mitarbeiter am längsten zu verstehen brauchen
- Funktionen, deren Änderungen auf unverwandte Module übergehen
Die Bereiche, die immer wieder auftauchen, sind Ihre Schuldenhotspots.
Messen Sie heute die Kosten von Änderungen
Eine ausgefeilte Tooling-Lösung ist nicht erforderlich. Es reicht aus, einige wenige Indikatoren zu verfolgen:
Average time to implement feature
Average PR size
Number of files touched
Regression frequency
Build time
Test execution time
Deployment frequency
Die Verknüpfung von Schulden mit solchen ingenieurtechnischen Ergebnissen macht es deutlich einfacher, Prioritäten zu setzen, denn man kann zeigen, dass ein bestimmter Bereich die Lieferung verlangsamt, anstatt zu behaupten, dessen Code sei unattraktiv.
Definieren Sie Abhängigkeitsregeln, bevor Sie ein Muster wählen
Bevor Sie über Clean Architecture oder Feature-Sliced Design diskutieren, klären Sie zunächst eine grundlegendere Frage: Was darf von was abhängen? Eine einfache schichtbasierte Struktur könnte wie folgt aussehen:
app
↓
features
↓
shared
Darin kann eine Funktion von gemeinsamen Bausteinen abhängen:
features/product
↓
shared/ui
shared/lib
Aber gemeinsam genutzter Code darf auf keinen Fall in eine Funktion zurückgreifen:
shared/ui
X
features/product
Die spezifische Architektur kann im Laufe der Zeit ändern; es ist die Richtung der Abhängigkeiten, die sie kohärent hält. Sobald eine gemeinsame Benutzeroberfläche anfängt, Inhalte aus einer Produktfunktion zu importieren, ist sie nicht mehr gemeinsam, und jeder Nutzer ist stillschweigend von dieser Funktion abhängig. Diese Regeln lassen sich außerdem leicht automatisch mit Lint-Regeln für Importpfade durchsetzen, wodurch eine architektonische Absicht zu etwas wird, das in jedem Pull Request von CI überprüft wird.
Eine funktionsorientierte Struktur für React und Next.js
Es gibt keine einzige Struktur, die für jedes Projekt richtig ist, aber die Organisation nach Funktionen schafft natürliche Grenzen:
src/
app/
routes/
providers/
layouts/
features/
checkout/
components/
hooks/
services/
state/
types/
products/
components/
hooks/
services/
state/
types/
users/
components/
hooks/
services/
state/
types/
shared/
ui/
hooks/
lib/
types/
Das Kopieren dieser Ordnernamen ist nicht das Wichtigste. Entscheidend ist, dass die Eigentumsverhältnisse klar definiert werden: app verbindet Routen, Provider und Layouts miteinander, jede Funktion besitzt ihre eigenen Komponenten, Hooks, Dienste, Zustände und Typen, und shared enthält nur das, was tatsächlich allgemeingültig ist. Für einen Vergleich von Alternativen siehe einen praktischen Vergleich der React-Ordnerstruktur-Muster.
Gewohnheiten, die man in großen Frontends vermeiden sollte
Beim Beitritt zu oder bei der Wartung einer großen React-Codebasis sollten Sie folgendes beachten:
- Standardmäßige Globalisierung des Zustands. Die meisten Werte müssen niemals ihren Komponenten verlassen.
- Verfrühte Generalisierung. Erst die Unterschiede verstehen, anschließend abstrahieren.
utils.ts öffnen. Finden Sie den Bereich, der die Logik beherbergen sollte.Von besseren Codezeilen zu veränderbaren Systemen
Der bedeutendste Wandel, der mit Erfahrung eintritt, ist eine Veränderung des Ziels und nicht eine neue Technik. Zunächst stellt sich die natürliche Frage, wie man einen bestimmten Codeabschnitt verbessern kann. Später wird die Frage darin bestehen, das System für denjenigen, der es später ändert, übersichtlicher zu gestalten. Diese Neupositionierung verändert die Herangehensweise an Komponenten und Abstraktionen, das State-Management mit Redux, TypeScript, Next.js, Styling, Tests, die Gesamtarchitektur sowie Abhängigkeiten. Anstatt einzelne Dateien zu verfeinern, konzentriert man sich darauf, die Fähigkeit des Systems zur Weiterentwicklung zu optimieren.
Schulden sind ein Werkzeug, kein Feind
Man kann technischen Schulden nicht vollständig entgehen – und es ist ein Fehler, es zu versuchen. Jedes Produkt erfordert Kompromisse, und viele davon erzeugen durch ihre Konzeption Schulden: schnelle Lieferung, gleichzeitige Unterstützung mehrerer Geschäftsanforderungen, Aufrechterhaltung von veralteten Browsern, Änderung der Produktrichtung oder Integration einer übernommenen Anwendung. Selbst eine hervorragende architektonische Entscheidung kann zu Schulden werden, sobald sich das umgebende System ändert.
Daher ist das Ziel „null Schulden“ falsch. Das realistische Ziel sind Schulden, die das Team versteht, unter Kontrolle hält und abtragen kann, sobald sie die Lieferung beeinträchtigen.
Was eigentlich dazu führt, dass mit einer großen Anwendung schwer gearbeitet werden kann
Es lohnt sich, genau hinschauen, welche Ursachen dahinterstecken – denn diese sind selten die Dinge, die die Leute dafür verantwortlich machen:
- Viele Komponenten sind nicht das Problem; vielmehr sind es die Beziehungen zwischen ihnen, die niemand nachvollziehen kann.
Kurz gesagt: Technischer Schuldenstand ist kein Code, der unschön aussieht. Es handelt sich um Code, der Änderungen unnötig teuer macht. Die beste Frontend-Architektur ist nicht die, die vom ersten Tag an perfekt aussieht, sondern die, die dem Team auch fünf Jahre später noch eine schnelle Weiterentwicklung ermöglicht.
Prülliste vor der Einführung einer neuen Funktion
Vor dem Hinzufügen einer neuen Funktion zu einer React-Codebasis sollten Sie sich folgende Fragen stellen:
- Gibt es einen echten Grund dafür, dass dieser Zustand global sein muss?
- Behält der Backend diese Daten, oder handelt es sich dabei ausschließlich um UI-Zustand?
- Nimmt das Komponente eine zweite oder dritte Verantwortung auf sich?
- Löst die neue Abstraktion ein tatsächliches Problem, oder sieht sie nur ordentlicher aus?
- Welche Funktion ist für diese Geschäftsregel verantwortlich?
- Könnte dieser Code stattdessen auf dem Server in Next.js ausgeführt werden?
- Ist hier wirklich ein Effect erforderlich?
- Führt das dazu, dass ein weiteres Paket gewartet werden muss?
- wird die Komponente tatsächlich in verschiedenen Funktionen verwendet?
- Könnte ein Neuling diesen Code verstehen, ohne den Autor zu fragen?
Diese Fragen sind in der Regel wertvoller als noch eine weitere Liste mit React-Best Practices.
Themen, die weiter untersucht werden sollten
Mehrere Abschnitte dieses Leitfadens verdienen eine eigene, ausführlichere Behandlung:
- Wann Redux tatsächlich die richtige Wahl ist im Vergleich zu lokalem State, Context, Redux Toolkit, RTK Query, speziellen Mechanismen zum Umgang mit Server-State, URL-State und Form-State.
- Refactoring eines 1.000-Zeilen-Komponenten ohne Störung der Produktivumgebung als schrittweises Fallbeispiel.
- Ob wiederverwendbare Komponenten eine App verschlechtern, mit Fokus auf Prop-Explosion, Komposition und Design-Systeme.
- Server- versus Client-Komponenten in Next.js, mit Schwerpunkt auf Entscheidungsgrundlagen an den Grenzen statt auf der Funktionsweise von
"use client". - TypeScript in großem Umfang, einschließlich
any, Generika, DTOs, Domain- und API-Typen, Duplikation und Inferenz. - Was den
utils-Ordner ersetzen sollte als Sammelstelle für Logik.
Change cost
Bug frequency
Dependency coupling
Component size
State complexity
Build time
Testability
Developer onboarding
Wichtige Erkenntnisse
- Frontend-Verbindlichkeiten entstehen durch kleine, vernünftige Entscheidungen; beobachten Sie den Trend, nicht einzelne Änderungen.
- Bewerten Sie den Zustand der Codebasis anhand der Kosten und des Risikos von Änderungen – nicht danach, ob die Anwendung derzeit funktioniert.
- Lassen Sie den Zustand standardmäßig lokal gespeichert, betrachten Sie Serverdaten als Caching-Problem und globalisieren Sie nur das, was tatsächlich cross-cutting ist.
- Ziehen Sie Grenzen sowie die Richtung von Abhängigkeiten vorübergehend vor Ordnernamen oder benannten Methodenansätzen vor.
- Reduzieren Sie die Verbindlichkeiten schrittweise an ausgewählten Problemstellen, anstatt auf eine komplette Neuschreibung zu setzen.