Startseite / Artikel

Feature-Sliced Design für React: Schichten, Importregeln und wann man aufhören sollte

Erfahren Sie, wie die Schichten von Feature-Sliced Design, die einseitigen Importregeln, die schmalen öffentlichen APIs sowie @x-Cross-Imports React-Codebasen entwirren und wann man nur einen Teil davon anwenden sollte.

Verlag: REACTAPP.TOP7 Min. Lesezeit1715 Wörter

Quellen: Feature-Sliced Design in React: How to Structure a Scalable Codebase (and When It’s Overkill)

Fast jede React-Codebasis, die länger als zwei Jahre überlebt, verfügt über ein utils/-Verzeichnis, das niemand öffnen möchte. Darin befinden sich ein Datumsformatierer, ein Wiederholungs-Wrapper, eine halbe Zustandsmaschine sowie ein helpers.ts, das aus drei Funktionsverzeichnissen importiert wird, während zwei dieser Verzeichnisse es wieder importieren – wodurch niemand genau sagen kann, was von was abhängt. Das Feature-Sliced Design (FSD) ist eine Methodik, die gezielt dieses Problem löst. Nach dem Lesen dieses Textes kennen Sie seine sechs Schichten, die einzige Importregel, die den größten Teil der Arbeit übernimmt, wie man Grenzen für Funktionsblöcke festlegt, die ein sicheres Refactoring ermöglichen, und vor allem, wie man entscheidet, ob ein vollständiges FSD für Ihre Anwendung sinnvoll ist oder ob ein leichteres Subset die bessere Wahl darstellt.

Warum typbasierte Verzeichnisse das Skalieren erschweren

Die klassische Struktur gruppiert Dateien nach ihrem Typ (Komponenten, Hooks, Utilities) und nicht nach ihrer Verwendungszweck. Das ist für eine kleine Anwendung in Ordnung, doch sobald echte Geschäftsbereiche hinzukommen, sieht es so aus:

src/
 components/ # 200 files, no domain grouping
 hooks/ # useAuth next to useCartTotals
 utils/ # the grab-bag
 services/ # api calls that import from utils, which imports back

In einer solchen Struktur verfallen zwei Dinge. Erstens hat die Frage „Wohin gehört diese Datei?“ keine klare Antwort, wodurch alles Unklare schließlich in utils/ landet. Zweitens gibt es keine Beschränkungen bezüglich der Richtung der Abhängigkeiten, was zum Entstehen von Zyklen führt. Stellen Sie sich vor, der Bestelldienst ruft utils/user.ts auf, diese Hilfsfunktion hängt wiederum von services/auth.ts ab, und der Authentifizierungsdienst importiert erneut den Bestelldienst. Solche Schleifen sind unauffällig. Sie treten in der Regel viel später auf, wenn ein Wert unerwartet undefined ist, sobald ein Modul zum ersten Mal geladen wird – weil eine Datei in der Schleife bereits vor Abschluss der von ihr abhängigen Datei ausgewertet wurde, und zu diesem Zeitpunkt ist das Abhängigkeitsnetzwerk bereits zu verschachtelt, um es noch zu durchschauen.

FSD behebt beide Probleme, indem es dem gesamten Projekt eine vorgegebene Struktur gibt, anstatt auf die Gewohnheiten einzelner Entwickler zu vertrauen. Zum Zeitpunkt der Erstellung ist v2.1 die aktuelle Version der Spezifikation; prüfen Sie die offiziellen Dokumente nach späteren Änderungen.

Die sechs Schichten und deren Inhalt

FSD organisiert den Code in Schichten mit fester Reihenfolge, von oben nach unten: App, Pages, Widgets, Features, Entities, Shared. Es existiert auch eine ältere Processes-Schicht, diese ist jedoch veraltet und kann ignoriert werden. Die Reihenfolge dient nicht nur ästhetischen Zwecken; sie bestimmt außerdem, wer von wem abhängen darf.

  • App: Routing, Eingangspunkte, globale Styles und Provider.
  • Pages: Vollständige Seiten oder große Abschnitte der Benutzeroberfläche, die mit einer Route verknüpft sind.
  • Widgets: Große, eigenständige Benutzeroberflächenblöcke, die einen ganzen Anwendungsfall für sich allein abdecken.
  • Funktionen: Implementierungen der Produktfunktionalität, die an mehreren Stellen wiederverwendet werden.
  • Entitäten: Die Geschäftskonzepte, mit denen sich das Projekt befasst, wie zum Beispiel ein Benutzer oder ein Produkt.
  • Gemeinsamer Code: Wiederverwendbarer Code ohne geschäftliches Wissen, einschließlich des Design-Systems und allgemeiner Hilfsfunktionen.
  • Die vier mittleren Schichten (Pages bis Entities) werden in Slices unterteilt, jeweils ein Slice pro Geschäftsdomäne – beispielsweise entities/user oder features/auth. App und Gemeinsamer Code haben keine Slices. Jeder Slice wird anschließend nach technischer Rolle in Segmente aufgeteilt: ui für Komponenten, api für Backend-Anfragen und Antworttypen, model für Schemata und Speicherstrukturen, lib für Hilfsfunktionen, die nur innerhalb dieses Slices verwendet werden, sowie config.

    Die Importregel, die Zyklen unmöglich macht

    Die Regel, die das gesamte System rechtfertigt, ist kurz: Ein Modul darf nur von Schichten importieren, die strikt unter seiner eigenen liegen. Eine Seite kann eine Funktion verwenden, aber eine Funktion darf niemals eine Seite verwenden. Darüber hinaus dürfen Schnitte auf derselben Schicht überhaupt nicht voneinander importieren. Innerhalb eines einzigen Schnitts können Dateien frei aufeinander verweisen; die Beschränkung gilt jedoch auch über Grenzen zwischen Schnitten und Schichten hinweg.

    Wenden wir diese Regel auf den zuvor beschriebenen Zyklus an. Nach der erneuten Aufteilung liegt die Logik für die Reihenfolge in entities/order, die Benutzerlogik in entities/user und der Authentifizierungsfluss in features/auth. Die zulässigen Abhängigkeiten lauten nun:

    features/auth → can import entities, shared
    entities/order → can import shared only
    entities/user → can import shared only
    

    Falls entities/order versucht, aus features/auth zu importieren, handelt es sich dabei um eine strukturelle Verletzung der Architektur und nicht um ein Stilproblem. Ein Zyklus kann sich nicht bilden, da jede Kante im Abhängigkeitsgraphen nach unten zeigt. Dadurch ändert sich auch die Art und Weise, wie man über den Code nachdenkt: Anstatt Importe Datei für Datei nachzuvollziehen, denkt man in Begriffen von sechs Schichten, was ein Modell ist, das sich eine Person tatsächlich merken kann.

    Öffentliche APIs halten die Interna der Slices privat

    Jede Slice stellt eine öffentliche API zur Verfügung, in der Regel ein index.ts, das das bereitstellt, was Außenstehende verwenden können. Die FSD-Dokumentation ist klar in ihrer Absicht: Nur das Notwendige wird bereitgestellt, während Implementierungsdetails verborgen bleiben.

    Eine sehr häufig vorkommende Version dieser Datei vereitelt jedoch den Zweck:

    // features/auth/index.ts (the anti-pattern)
    export * from "./ui";
    export * from "./model";
    

    Die Wildcards exportieren alles erneut, einschließlich von model/tokenStore.ts, das eigentlich privat bleiben sollte. Früher oder später importiert irgendeine Seite den Token-Store direkt, und ab dann kann man ihn nicht mehr umstrukturieren, ohne einen bisher unbekannten Nutzer zu beeinträchtigen. Wildcard-Exporte können außerdem dazu führen, dass Bundler Schwierigkeiten haben, unnötigen Code zu entfernen, wodurch ungenutzter Code schließlich an Nutzer ausgeliefert werden kann.

    Die Alternative listet jeden Export explizit auf:

    // features/auth/index.ts (the boundary)
    export { LoginForm } from "./ui/LoginForm";
    export { useSession } from "./model/useSession";
    // tokenStore stays internal. Nobody outside auth can touch it.
    

    Die Struktur weist nun genau zwei öffentliche Namen auf. Alles andere darin kann umbenannt, geteilt oder umgeschrieben werden, ohne die Aufrufer zu beeinflussen. Diese Kombination aus einer verständlichen Abhängigkeitsstruktur und der Freiheit, die Interna zu ändern, ist der wesentliche Vorteil, den FSD verspricht.

    Es gibt eine Einschränkung, die in der FSD-Dokumentation selbst erwähnt wird: Die index-Dateien der Barrel können zyklische Importe verursachen, Probleme beim Tree-Shaking in shared/ui und shared/lib hervorrufen sowie die Bundler in großen Projekten verlangsamen. Die praktische Lösung besteht darin, öffentliche APIs explizit und minimal zu halten sowie besonders vorsichtig in den Bereichen Shared vorzugehen, wo viele kleine, unabhängige Module besser direkt importiert werden sollten anstatt über ein großes Barrel weitergeleitet zu werden.

    Kreuzimporte, übermäßiges Aufteilen und das Zulassen von Einsprüchen durch Linter

    Die @x-Notation für voneinander abhängige Entities

    Einige Entitäten sind berechtigterweise voneinander abhängig; ein Auftrag benötigt beispielsweise den User-Typ. Da Importe auf derselben Ebene verboten sind, hat v2.1 die @x-Notation genau für diese Situation standardisiert. Der user-Slice stellt einen speziellen Eingangspunkt für den order-Slice bereit, und order importiert davon:

    // entities/user/@x/order.ts
    export type { User } from "../model/types";
    
    // in entities/order/model/types.ts
    import type { User } from "entities/user/@x/order";
    

    Der Dateinamen macht die Beziehung klar und suchbar: entities/user/@x/order.ts ist der Teil von user, den order einsehen darf. In den Dokumentationen wird dies als seltene Ausnahme betrachtet. Halten Sie Kreuzimporte auf ein Minimum und verwenden Sie @x nur in der Entities-Ebene. Wenn Sie feststellen, dass er im gesamten Codebase benötigt wird, ist das ein Zeichen dafür, dass Ihre Slices an den falschen Stellen geteilt sind.

    Steiger weist sowohl Verstöße als auch Überengineering auf

    Regeln helfen nur, wenn sie durchgesetzt werden. Steiger, der Architektur-Checker für FSD (seine Regeln werden als @feature-sliced/steiger-plugin bereitgestellt), funktioniert ohne Konfiguration und war zum Zeitpunkt der Erstellung noch in Beta. Neben der Überprüfung der Importregeln wirkt er auch gegen übermäßige Komplexität: Die insignificant-slice-Regel tritt ein, wenn eine Funktion oder Einheit nur von einer einzigen Seite verwendet wird, während excessive-slicing darauf hinweist, wenn das Projekt in zu viele kleine Teile aufgeteilt wurde. Wenn Steiger auf eine unbedeutende Aufteilung hinweist, sollten Sie darauf achten – Sie haben möglicherweise eine Dateistruktur erstellt, für die eine lokale Funktion ausgereicht hätte.

    Wann FSD überflüssig ist

    Die Ordnernamen sind der am wenigsten wichtige Teil von FSD. Das Kopieren aller sechs Ebenen in eine kleine Anwendung erzeugt eine eigene Art von Unordnung: mehr Dateien als Logik, und jede Funktion ist über vier Verzeichnisse verteilt, sodass eine einfache Änderung zu einer Suche wird.

    Die Spezifikation erkennt dies, weshalb v2.1 einen „Pages first“-Ansatz für die Einführung empfiehlt. Beginnen Sie mit App, Pages, Widgets und Shared und überlegen Sie, dort aufzuhören. Für viele Anwendungen ist das bereits eine vollständige und übersichtliche Struktur.

    Bringen Sie Code nur in Features und Entities, wenn ein konkreter Grund vorliegt. Eine nützliche Heuristik ist es, zu warten, bis dieselbe Logik von drei oder mehr Seiten oder Widgets benötigt wird, bevor man ihr einen eigenen Bereich zuweist. Bis dahin ist ein Checkout-Fluss, der vollständig in pages/checkout liegt, die richtige Wahl – kein Abkürzungsweg. Eine einzige Wiederverwendung kann Zufall sein; drei deuten auf ein echtes Muster hin, und genau dafür existiert die Features-Schicht.

    Falls Sie auch FSD mit einfacheren Strukturen wie Feature-Verzeichnissen oder domänenbasierten Layouts vergleichen, behandelt unsere Vergleichsstudie zu React-Verzeichnisstrukturmustern die Alternativen nebeneinander.

    Kurze Checkliste zur Einführung

    • Führen Sie FSD vollständig ein, sobald das Team aus mehr als nur wenigen Entwicklern besteht, die Anzahl der Geschäftsbereiche weiter wächst und utils/ bereits zu einem Auffangbecken für alles geworden ist.
    • Für ein kleines Produkt mit einem klaren Verantwortlichen sollten Sie die Richtlinie zur nach unten gerichteten Importierung beibehalten, die öffentlichen APIs schrumpfen lassen und auf zusätzliche Schichten verzichten.
    • Schreiben Sie jeden index.ts als explizite Liste von Exporten; vermeiden Sie export *.
    • Reservieren Sie @x für Referenzen von Entity zu Entity und betrachten Sie häufige Verwendung als Grenzproblem.
    • Führen Sie einen Linter wie Steiger aus, damit die Regeln sowie Warnungen bezüglich unnötiger Slice automatisch angewendet werden.

    Zusammenfassung

    Sechs Ordner waren nie das Ziel. Was FSD wirklich bietet, ist ein Abhängigkeitsgraphen, der nur in eine Richtung fließt, sowie Schnittebenen, deren Interna hinter kleinen Verträgen verborgen sind – diese zusammen sorgen dafür, dass die Codebasis auch in einem Jahr noch verständlich bleibt. Den größten Teil dieses Nutzens kann man bereits allein durch die Importregeln und öffentlichen APIs erlangen; zusätzliche Schichten sollten nur hinzugefügt werden, wenn tatsächlich eine Wiederverwendung erforderlich ist.