2026

21. März 2026

React-Projektstruktur: Von MANTRA zu modernen Frameworks

Ein Ruckblick darauf, wie wir 2017 React-Apps mit der MANTRA-Architektur strukturiert haben, welche Probleme sie loste und wie moderne Frameworks wie Next.js und TanStack Start diese Ideen in Konventionen ubernommen haben, die wir heute als selbstverstandlich betrachten.

S
Sascha Becker
Author

11 Min. Lesezeit

React-Projektstruktur: Von MANTRA zu modernen Frameworks

React-Projektstruktur: Von MANTRA zu modernen Frameworks

2017 habe ich einen Artikel geschrieben: Structure Your React Apps the Mantra Way. Es war die Zeit von Create React App, Redux-Boilerplate und react-router-dom v4. Next.js existierte zwar, war aber noch eine Nische. Ordnerstrukturen waren der Wilde Westen.

Der Artikel schlug eine modulbasierte Architektur vor, inspiriert von Mantra JS, einer Anwendungsspezifikation von Kadira aus der Meteor-Ara. Sie war eigenwillig, strukturiert und loste echte Probleme, mit denen Teams taglich zu kampfen hatten. Wenn ich den Artikel fast ein Jahrzehnt spater nochmal lese, fallt mir auf, wie viele dieser Ideen zum Mainstream geworden sind und wie viel Zeremonie moderne Frameworks eliminiert haben.

Dieser Beitrag ist eine Retrospektive. Woher wir kamen, wo wir jetzt stehen und warum der Weg dorthin wichtig ist.

Das Problem 2017

React gab dir Komponenten und eine Render-Funktion. Alles andere war dein Problem. Wie man Daten abruft, wo der State hinkommt, wie man Routen verdrahtet und, am umstrittensten, wie man seine Dateien organisiert. Dan Abramov fasste die Stimmung treffend zusammen mit einer eigenen Website, deren gesamter Ratschlag lautete: "move files around until it feels right." Das war kein Witz. Es gab schlicht keinen Konsens.

Der erste Instinkt war, nach Dateityp zu gruppieren:

src
components
Games.js
GameTile.js
Header.js
containers
GamesContainer.js
HomeContainer.js
actions
gameActions.js
coreActions.js
reducers
gameReducer.js
coreReducer.js
routes
AppRoutes.js

Das sieht anfangs ordentlich aus. Es zerfallt in dem Moment, in dem man zwanzig Features hat. Muss man andern, wie Games funktionieren? Funf Ordner anfassen. Ein Feature loschen? Viel Gluck, alle Teile zu finden.

Wie ich damals schrieb:

Der MANTRA-Ansatz

MANTRA drehte die Struktur von "gruppieren nach Typ" auf "gruppieren nach Geschaftslogik." Jedes Feature wurde ein eigenstandiges Modul mit eigenen Komponenten, Containern, Actions, Reducern und Routen:

modules
core
components
containers
actions
reducers
routes
index.js
games
components
containers
actions
reducers
routes
index.js
contact
components
containers
actions
reducers
routes
index.js

Die Regeln waren einfach:

  1. Geschaftslogik zuerst. Jedes Modul besitzt alles, was es braucht.
  2. Keine kaskadierten Module. Wenn ein Modul ein Submodul braucht, wird es als Geschwister erstellt, nicht als Kind. Wie ich es formulierte: "you don't cascade modules. If a module should have a submodule create it separately. The advantage is that you see all modules at a glance."
  3. Explizite offentliche API. Jedes Modul stellt seine Teile uber eine index.js bereit:
js
import * as actions from "./actions";
import reducers from "./reducers";
import routes from "./routes";
export { actions, reducers, routes };
  1. Verbindung an der Spitze. Eine Root-Datei kombinierte alle Modul-Reducer, eine andere verknupfte alle Routen:
AppRoutes.js
import { routes as home } from "./modules/home";
import { routes as games } from "./modules/games";
import { routes as team } from "./modules/team";
export default (store) => {
return (
<Switch>
<Application>
{home(store)}
{games(store)}
{team(store)}
</Application>
</Switch>
);
};

Die Vorteile waren real. Isolation bedeutete, dass man ein Modul austauschen konnte, ohne andere zu brechen. Import-Pfade blieben kurz. Die Arbeit an einem Feature belastete nicht das mentale Modell eines anderen. Wie ich zusammenfasste:

Was wir wirklich gelost haben

Ruckblickend loste MANTRA vier verschiedene Probleme, die React und sein Okosystem offen liessen:

  1. Feature-Isolation. Wo lebt der gesamte Code fur "Games"? An einem Ort, nicht verstreut uber typbasierte Ordner.
  2. Route-Komposition. Wie fugen wir eine neue Seite hinzu? Indem wir ein Modul erstellen und seine Routen in einer Verbindungsdatei registrieren.
  3. State-Grenzen. Jedes Modul besitzt seine Reducer und Actions. Keine globale Suppe aus unzusammenhangenden States.
  4. Datenfluss-Klarheit. Die Container/Component-Trennung machte explizit, woher Daten kamen und wo die Prasentation lebte.

Jedes einzelne dieser Probleme wurde seitdem durch Framework-Konventionen adressiert.

Die moderne Antwort

Dateisystem-Routing ersetzte manuelles Route-Wiring

2017 exportierte jedes Modul eine Route-Funktion, die <Route>-Komponenten zuruckgab, und eine Top-Level-Datei nahte sie zusammen. Es funktionierte, aber eine neue Seite hinzuzufugen bedeutete, mindestens zwei Dateien zu bearbeiten: die Routen des Moduls und die zentrale AppRoutes.js.

Next.js App Router, TanStack Start und React Router (v7, Framework-Modus) nutzen alle das Dateisystem als Router. Datei erstellen, Route bekommen:

app
(auth)
login
page.tsx
register
page.tsx
layout.tsx
(dashboard)
layout.tsx
games
page.tsx
loading.tsx
_components
GameTile.tsx
actions.ts
orders
page.tsx
_components
OrderTable.tsx
actions.ts
(marketing)
layout.tsx
page.tsx
pricing
page.tsx

Die Verbindungsdatei ist weg. Die Modulgrenze ist einfach ein Ordner. Route-Registrierung ist implizit.

Server Components ersetzten das Container-Pattern

Die Container/Presentational-Trennung war das dominante React-Pattern 2017. Container verbanden sich mit Redux, holten Daten und reichten sie nach unten. Presentational Components waren "dumm" und renderten nur Props.

js
// 2017: Container verbindet sich mit Redux und reicht Daten weiter
class Container extends Component {
componentDidMount() {
this.props.dispatch(coreActions.setMenuIndex(1));
}
render() {
return <Games {...this.props} />;
}
}
export default connect((state) => {
return { mobile: state.core.responsive.mobile };
})(Container);

Mit React Server Components ist die Server-Komponente die Datenschicht. Kein Wrapper notig:

tsx
// 2026: Server Component fetcht direkt
export default async function GamesPage() {
const games = await getGames();
return <GameList games={games} />;
}

Mit TanStack Start ubernimmt ein Loader auf der Route-Definition die gleiche Aufgabe:

tsx
// TanStack Start
export const Route = createFileRoute("/games")({
loader: async () => ({ games: await getGames() }),
component: GamesPage,
});

React Router v7 (ehemals Remix) nutzt ein ahnliches Konzept mit einem benannten Export:

tsx
// React Router v7 (Framework-Modus)
export const loader = async () => {
const games = await getGames();
return { games };
};

Das Container-Pattern starb nicht, weil es falsch war. Es starb, weil das Framework seine Verantwortung ubernahm.

Kolokierte Actions ersetzten Redux-Module

Jedes MANTRA-Modul hatte eigene actions/- und reducers/-Ordner. Action Types, Action Creators und Reducer-Funktionen waren uber mehrere Dateien pro Feature verteilt. Eine einzige Benutzerinteraktion hinzuzufugen bedeutete, drei oder vier Dateien anzufassen.

actionTypes.js
export const MENU_TOGGLE = "MENU_TOGGLE";
export const SET_MENU_INDEX = "SET_MENU_INDEX";
// actions.js
export function toggleMenu(open) {
return { type: TYPES.MENU_TOGGLE, open };
}
// reducer.js
export default function (state = defaultState, action) {
switch (action.type) {
case TYPES.MENU_TOGGLE:
return toggleMenu(state, action);
// ...
}
}

Heute leben Server Actions direkt neben der Seite, die sie nutzt:

app/games/actions.ts
"use server";
export async function toggleFavorite(gameId: string) {
await db.game.update({ where: { id: gameId }, data: { favorite: true } });
revalidatePath("/games");
}

Eine Datei. Keine Action Types, kein Dispatch, kein Reducer-Boilerplate. Fur Client State ersetzt ein kleiner Zustand- oder Jotai-Store, was fruher ein ganzes Redux-Modul war.

Das Index.js-Pattern wurde uberflussig

MANTRAs index.js pro Modul war die offentliche API: Sie re-exportierte Actions, Reducer und Routen, damit andere Module uber einen sauberen Pfad importieren konnten. Das war gute Praxis fur die Aufrechterhaltung von Grenzen.

In einem Framework mit Dateisystem-Konventionen werden diese Grenzen vom Framework selbst durchgesetzt. Dateien wie page.tsx und layout.tsx haben jeweils eine bekannte Rolle. Es gibt nichts zu re-exportieren, weil das Framework weiss, wo es suchen muss.

Was sich bewahrt hat

Nicht alles musste ersetzt werden. Einige von MANTRAs Ideen sind heute akzeptierte Weisheit.

Feature-basierte Organisation ist der Standard. Ob man sie Module, Features oder Route Segments nennt: Die Industrie hat sich auf "gruppieren nach Geschaftslogik" geeinigt. Der Next.js App Router ist im Wesentlichen MANTRAs Modulstruktur, durchgesetzt durch Konvention.

Flache Modul-Hierarchien. Die Regel gegen kaskadierte Module passt direkt dazu, wie Route Groups in modernen Frameworks funktionieren. Tief verschachtelte Feature-Ordner sind immer noch ein Anti-Pattern. Dinge flach und auf einen Blick sichtbar zu halten, ist immer noch guter Rat.

Kolokation. Komponenten, ihre Datenlogik, ihre Styles und ihre Tests leben nebeneinander. Das war 2017 ein neuartiger Ratschlag. 2026 ist es selbstverstandlich.

Explizite Grenzen zwischen Features. Auch ohne index.js-Re-Exports bleibt das Prinzip bestehen, Module voneinander zu isolieren. Ob man es durch Ordner-Konventionen, Barrel Files oder ESLint-Import-Regeln durchsetzt: Die Idee ist dieselbe.

Was ich meinem 2017er-Ich sagen wurde

Die Architektur war solide. Der Instinkt, nach Feature zu organisieren, Module flach zu halten und klare Grenzen zu pflegen, war genau richtig. Die Umsetzung erforderte nur eine Menge manueller Klempnerarbeit, die Frameworks heute ubernehmen.

Ich habe damals auch ein npm-Paket namens module-loader geschrieben, um den Setup-Aufwand zu automatisieren. Es war der richtige Impuls: Das Boilerplate war der schwachste Teil. Frameworks kamen letztlich zum gleichen Schluss und eliminierten es komplett.

Wenn du heute ein neues React-Projekt startest, musst du uber das meiste davon nicht nachdenken. Nimm Next.js, TanStack Start oder React Router. Folge den Datei-Konventionen. Deine Projektstruktur ist bereits besser als das, woruber wir 2017 wochenlang diskutiert haben.

Aber wenn du an einer grossen SPA ohne Framework arbeitest (und es gibt immer noch gute Grunde dafur), halten die MANTRA-Prinzipien stand. Gruppiere nach Feature. Halte Module flach. Mache Grenzen explizit. Die Namen andern sich, die Idee nicht.

MANTRA mit Vite (ohne Framework)

Wenn du Vite oder Vite+ ohne ein Meta-Framework verwendest, gibt es keine Dateisystem-Konventionen, auf die du dich stutzen kannst. Du bist wieder in CRA-Territorium, und MANTRAs Struktur lasst sich fast direkt ubertragen, nur mit modernen Werkzeugen anstelle der 2017er-Aquivalente:

src
features
games
components
GameTile.tsx
GameList.tsx
hooks
useGames.ts
api
games.queries.ts
routes.tsx
index.ts
orders
components
OrderTable.tsx
hooks
useOrders.ts
api
orders.queries.ts
routes.tsx
index.ts
shared
components
Layout.tsx
hooks
useAuth.ts

Die Form ist vertraut, aber die Inhalte haben sich verandert:

  • hooks/ ersetzt containers/. Custom Hooks haben die Datenabruf- und State-Logik ubernommen, die Container fruher gehandhabt haben. Keine Class Components mehr, die Class Components wrappen.
  • api/ ersetzt actions/ + reducers/. TanStack Query oder SWR ersetzt Redux fur Server State, sodass jedes Feature nur noch Query- und Mutation-Definitionen hat statt Action Types, Action Creators und Reducer-Funktionen.
  • index.ts ist weiterhin wichtig. Ohne Framework-Konventionen, die Grenzen durchsetzen, ist die Barrel-Datei wieder deine offentliche API, genau wie MANTRAs ursprungliche index.js.
  • routes.tsx muss weiterhin manuell verdrahtet werden. Du musst Feature-Routen immer noch in einem Root-Router zusammenfugen, genau wie in der alten AppRoutes.js. React Router oder TanStack Router ubernehmen das Rendering, aber die Komposition liegt bei dir.

Die Zeremonie ist leichter (kein Redux-Boilerplate, keine Container/Presentational-Trennung), aber die organisatorische Disziplin liegt weiterhin bei dir. Das ist der Kompromiss, wenn man ohne Framework arbeitet: mehr Freiheit, mehr Verantwortung.

Damals und Heute

Aspekt2017 (MANTRA)2026 (Frameworks)
Route-RegistrierungManuelle Verbindungsdatei mit Modul-RoutenDateisystem-Routing
DatenabrufContainer-Komponenten + Redux connectServer Components, Loader, Server Actions
State ManagementRedux Actions + Reducer pro ModulServer Actions + leichtgewichtige Stores (Zustand, Jotai)
Modulgrenzeindex.js-Re-ExportsOrdner-Konventionen + Framework-Dateirollen
Feature-IsolationManuelle DisziplinDurch Dateisystem-Struktur durchgesetzt
Geteilte LogikCore-Modul mit geteilten ActionsGeteilte Layouts, Middleware, Utility-Ordner
Build-ToolingCreate React App, Webpack-KonfigurationVite, Turbopack, Zero-Config

Die Tabelle lasst es wie einen sauberen Austausch aussehen, und in vielen Aspekten ist es das. Aber die mentalen Modelle hinter der 2017er-Spalte sind es, die die 2026er-Spalte moglich gemacht haben. Framework-Autoren haben Feature-basierte Organisation nicht erfunden. Sie haben beobachtet, was Teams bereits taten (oft muhsam, mit manuellem Boilerplate) und es in Konventionen verwandelt.

Schlussgedanke

Ich bin dankbar fur die MANTRA-Ara. Nicht weil der Code besser war (war er nicht), sondern weil das Denken richtig war. Wir haben echte Probleme mit den Werkzeugen gelost, die wir hatten. Dass diese Losungen so weit verbreitet wurden, dass sie in Framework-Konventionen verschwunden sind, ist das bestmogliche Ergebnis.

Jede page.tsx, die du erstellst, ohne daruber nachzudenken, ist ein Problem, das jemand 2017 hart erkampft hat.


S
Geschrieben von
Sascha Becker
Weitere Artikel