March 21, 2026
React Project Structure: From MANTRA to Modern Frameworks
A retrospective on how we structured React apps in 2017 with the MANTRA architecture, what problems it solved, and how modern frameworks like Next.js and TanStack Start have absorbed those ideas into conventions we now take for granted.
Sascha Becker
Author11 min read

React Project Structure: From MANTRA to Modern Frameworks
Back in 2017, I wrote an article called Structure Your React Apps the Mantra Way. It was the era of Create React App, Redux boilerplate, and react-router-dom v4. Next.js existed but was still niche. Folder structures were the wild west.
The article proposed a module-based architecture inspired by Mantra JS, an application specification developed by Kadira during the Meteor era. It was opinionated, structured, and solved real problems that teams were hitting every day. Reading it again almost a decade later, I'm struck by how many of those ideas became mainstream, and how much ceremony modern frameworks have eliminated.
This post is a retrospective. Where we came from, where we are now, and why the journey matters.
The Problem in 2017
React gave you components and a render function. Everything else was your problem. How to fetch data, where to put state, how to wire routes, and, most contentiously, how to organize your files. Dan Abramov famously captured the mood with a dedicated site whose entire advice was: "move files around until it feels right." It wasn't a joke. There simply was no consensus.
The default instinct was to group by file type:
srccomponentsGames.jsGameTile.jsHeader.jscontainersGamesContainer.jsHomeContainer.jsactionsgameActions.jscoreActions.jsreducersgameReducer.jscoreReducer.jsroutesAppRoutes.js
This looks tidy at first. It falls apart the moment you have twenty features. Need to change how games work? Touch five folders. Want to delete a feature? Good luck finding all the pieces.
As I wrote back then:
The MANTRA Approach
MANTRA flipped the structure from "group by type" to "group by business concern." Each feature became a self-contained module with its own components, containers, actions, reducers, and routes:
modulescorecomponentscontainersactionsreducersroutesindex.jsgamescomponentscontainersactionsreducersroutesindex.jscontactcomponentscontainersactionsreducersroutesindex.js
The key rules were simple:
- Business concern first. Each module owns everything it needs.
- No cascading modules. If a module needs a submodule, create it as a sibling, not a child. As I put it: "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."
- Explicit public API. Each module exposes its parts through an
index.js:
jsimport * as actions from "./actions";import reducers from "./reducers";import routes from "./routes";export { actions, reducers, routes };
- Glue at the top. A root file combined all module reducers, another stitched all routes together:
AppRoutes.jsimport { 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>);};
The benefits were real. Isolation meant you could swap out a module without breaking others. Import paths stayed short. Working on one feature didn't pollute your mental model with another. As I summarized:
What We Were Really Solving
Looking back, MANTRA was solving four distinct problems that React and its ecosystem left wide open:
- Feature isolation. Where does all the code for "games" live? In one place, not scattered across type-based folders.
- Route composition. How do we add a new page? By creating a module and registering its routes in one glue file.
- State boundaries. Each module owns its reducers and actions. No global soup of unrelated state.
- Data flow clarity. The container/component split made it explicit where data came from and where presentation lived.
Every single one of these problems has since been addressed by framework conventions.
The Modern Answer
File-System Routing Replaced Manual Route Wiring
In 2017, every module exported a route function that returned <Route> components, and a top-level file stitched them together. It worked, but adding a new page meant editing at least two files: the module's routes and the central AppRoutes.js.
Next.js App Router, TanStack Start, and React Router (v7, framework mode) all use the file system as the router. Create a file, get a route:
app(auth)loginpage.tsxregisterpage.tsxlayout.tsx(dashboard)layout.tsxgamespage.tsxloading.tsx_componentsGameTile.tsxactions.tsorderspage.tsx_componentsOrderTable.tsxactions.ts(marketing)layout.tsxpage.tsxpricingpage.tsx
Next.js Conventions
Folders in parentheses like (auth) are route groups: they organize code without affecting the URL. Folders prefixed with _ like _components are private folders: they opt out of routing entirely. Both are Next.js conventions for keeping feature code colocated.
The glue file is gone. The module boundary is just a folder. Route registration is implicit.
Server Components Replaced the Container Pattern
The container/presentational split was the dominant React pattern of 2017. Containers connected to Redux, fetched data, and passed it down. Presentational components were "dumb" and only rendered props.
js// 2017: container connects to Redux and passes data downclass 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);
With React Server Components, the server component is the data layer. No wrapper needed:
tsx// 2026: server component fetches directlyexport default async function GamesPage() {const games = await getGames();return <GameList games={games} />;}
With TanStack Start, a loader on the route definition serves the same purpose:
tsx// TanStack Startexport const Route = createFileRoute("/games")({loader: async () => ({ games: await getGames() }),component: GamesPage,});
React Router v7 (formerly Remix) uses a similar concept with a named export:
tsx// React Router v7 (framework mode)export const loader = async () => {const games = await getGames();return { games };};
The container pattern didn't die because it was wrong. It died because the framework absorbed its responsibility.
Colocated Actions Replaced Redux Modules
Each MANTRA module had its own actions/ and reducers/ folders. Action types, action creators, and reducer functions were spread across multiple files per feature. Adding a single user interaction meant touching three or four files.
actionTypes.jsexport const MENU_TOGGLE = "MENU_TOGGLE";export const SET_MENU_INDEX = "SET_MENU_INDEX";// actions.jsexport function toggleMenu(open) {return { type: TYPES.MENU_TOGGLE, open };}// reducer.jsexport default function (state = defaultState, action) {switch (action.type) {case TYPES.MENU_TOGGLE:return toggleMenu(state, action);// ...}}
Today, server actions live right next to the page that uses them:
app/games/actions.ts"use server";export async function toggleFavorite(gameId: string) {await db.game.update({ where: { id: gameId }, data: { favorite: true } });revalidatePath("/games");}
One file. No action types, no dispatch, no reducer boilerplate. For client state, a small Zustand or Jotai store replaces what used to be an entire Redux module.
The Index.js Pattern Became Unnecessary
MANTRA's index.js per module was the public API: it re-exported actions, reducers, and routes so other modules could import from a clean path. This was good practice for maintaining boundaries.
In a framework with file-system conventions, those boundaries are enforced by the framework itself. Files like page.tsx and layout.tsx each have a known role. There's nothing to re-export because the framework knows where to look.
What Aged Well
Not everything needed replacing. Some of MANTRA's ideas are now accepted wisdom.
Feature-based organization is the default. Whether you call them modules, features, or route segments, the industry settled on "group by business concern." The Next.js App Router is essentially MANTRA's module structure, enforced by convention.
Flat module hierarchies. The rule against cascading modules maps directly to how route groups work in modern frameworks. Deeply nested feature folders are still an antipattern. Keeping things flat and visible at a glance is still good advice.
Colocation. Components, their data logic, their styles, and their tests living next to each other. This was novel advice in 2017. It's table stakes in 2026.
Explicit boundaries between features. Even without index.js re-exports, the principle of keeping modules isolated from each other persists. Whether you enforce it through folder conventions, barrel files, or ESLint import rules, the idea is the same.
What I'd Tell My 2017 Self
The architecture was sound. The instinct to organize by feature, keep modules flat, and maintain clear boundaries was exactly right. The execution just required a lot of manual plumbing that frameworks now handle.
I also wrote an npm package called module-loader to automate the setup overhead. It was the right impulse: the boilerplate was the weakest part. Frameworks eventually came to the same conclusion and eliminated it entirely.
If you're starting a new React project today, you don't need to think about most of this. Pick Next.js, TanStack Start, or React Router. Follow the file conventions. Your project structure is already better than what we spent weeks debating in 2017.
But if you're working on a large SPA without a framework (and there are still good reasons to do so), the MANTRA principles hold up. Group by feature. Keep modules flat. Make boundaries explicit. The names change, the idea doesn't.
MANTRA with Vite (No Framework)
If you're using plain Vite or Vite+ without a meta-framework, there are no file-system conventions to lean on. You're back in CRA territory, and MANTRA's structure translates almost directly, just with modern tools replacing the 2017 equivalents:
srcfeaturesgamescomponentsGameTile.tsxGameList.tsxhooksuseGames.tsapigames.queries.tsroutes.tsxindex.tsorderscomponentsOrderTable.tsxhooksuseOrders.tsapiorders.queries.tsroutes.tsxindex.tssharedcomponentsLayout.tsxhooksuseAuth.ts
The shape is familiar, but the internals have changed:
hooks/replacescontainers/. Custom hooks absorbed the data fetching and state logic that containers used to handle. No more class components wrapping class components.api/replacesactions/+reducers/. TanStack Query or SWR replaces Redux for server state, so each feature just has query and mutation definitions instead of action types, action creators, and reducer functions.index.tsstill matters. Without framework conventions to enforce boundaries, the barrel file is your public API again, exactly like MANTRA's originalindex.js.routes.tsxstill needs manual wiring. You'll still stitch feature routes together in a root router, just like the oldAppRoutes.js. React Router or TanStack Router handle the rendering, but you do the composition.
The ceremony is lighter (no Redux boilerplate, no container/presentational split), but the organizational discipline is still on you. That's the trade-off of going frameworkless: more freedom, more responsibility.
Then and Now
| Concern | 2017 (MANTRA) | 2026 (Frameworks) |
|---|---|---|
| Route registration | Manual glue file importing module routes | File-system routing |
| Data fetching | Container components + Redux connect | Server Components, loaders, server actions |
| State management | Redux actions + reducers per module | Server actions + lightweight stores (Zustand, Jotai) |
| Module boundary | index.js re-exports | Folder conventions + framework file roles |
| Feature isolation | Manual discipline | Enforced by file-system structure |
| Shared logic | Core module with shared actions | Shared layouts, middleware, utility folders |
| Build tooling | Create React App, Webpack config | Vite, Turbopack, zero-config |
The table makes it look like a clean replacement, and in many ways it is. But the mental models behind the 2017 column are what made the 2026 column possible. Framework authors didn't invent feature-based organization. They observed what teams were already doing (often painfully, with manual boilerplate) and turned it into conventions.
Closing Thought
I'm grateful for the MANTRA era. Not because the code was better (it wasn't), but because the thinking was right. We were solving real problems with the tools we had. The fact that those solutions became so mainstream that they disappeared into framework conventions is the best possible outcome.
Every page.tsx you create without thinking about it is a problem someone fought to solve in 2017.
- Structure Your React Apps the Mantra Way (2017)
The original article that inspired this retrospective.
- Mantra JS Specification
The application architecture spec by Kadira for Meteor, which inspired the module-based approach.
- React File Structure by Dan Abramov
The famous one-liner that captured the state of the folder structure debate: 'move files around until it feels right.'
- Next.js App Router Documentation
The file-system based router that absorbed many of the patterns MANTRA established manually.
- TanStack Start
A modern full-stack React framework with file-based routing and type-safe server functions.
