Full-project lesson · needs Node.js, npm and real project files, so it does not run in the in-page playground.
Grouping by kind (all components together, all hooks together) works at first. As an app grows to dozens of screens, one change touches files in five folders, and nobody knows which components belong to what. Feature-based structure groups by what the code is for.
The idea
A feature is a slice of the product that users recognise: products, cart, checkout, account, search. Each feature folder holds everything for that slice: its components, hooks, API calls, utilities and tests.
src/
├── app/ app setup: router, providers, layout
│ ├── App.jsx
│ └── routes.jsx
├── features/
│ ├── products/
│ │ ├── components/
│ │ │ ├── ProductList.jsx
│ │ │ └── ProductCard.jsx
│ │ ├── api.js
│ │ ├── useProducts.js
│ │ ├── ProductsPage.jsx
│ │ └── index.js the feature's public API
│ ├── cart/
│ │ └── ...
│ └── account/
│ └── ...
├── shared/ code used by several features
│ ├── components/ Button, Modal, Layout
│ ├── hooks/
│ └── utils/
└── main.jsx
Why it helps
- Change locality. A task like “add a discount to the cart” touches
features/cartonly. - Easy to find. New developers look for the feature, not for a kind of file.
- Easy to delete. Removing a feature means removing a folder.
- Clear ownership. Teams can own features.
- Encourages boundaries. A feature exposes a few things and hides the rest.
The public API of a feature
Each feature has an index.js that exports only what other parts of the app may use:
export { default as ProductsPage } from './ProductsPage.jsx'
export { useProducts } from './useProducts.js'
Other code imports from the feature folder, not from its inside:
import { ProductsPage } from '../features/products' // good
import ProductCard from '../features/products/components/ProductCard.jsx' // reaching inside: avoid
That keeps internals free to change.
Dependency rules
Without rules, features tangle together. A simple set:
sharedimports from nothing infeaturesorapp.- A feature may import from
sharedand, sparingly, from another feature’s public API. appimports from features and shared to assemble the application.- Features do not import each other’s internals, and avoid cycles.
If two features keep needing the same code, move it to shared (or merge the features).
A verified mini example
This structure was built with Vite and tested in a browser: the app shell wires a route to a feature through its public API.
import { useState } from 'react'
const INITIAL = [
{ id: 1, name: 'Keyboard' },
{ id: 2, name: 'Mouse' },
]
export function useProducts() {
const [items] = useState(INITIAL)
return { products: items }
}
function ProductCard({ name }) {
return <li>{name}</li>
}
export default ProductCard
import ProductCard from './components/ProductCard.jsx'
import { useProducts } from './useProducts.js'
function ProductsPage() {
const { products } = useProducts()
return (
<section>
<h1>Products feature</h1>
<ul>
{products.map((p) => <ProductCard key={p.id} name={p.name} />)}
</ul>
</section>
)
}
export default ProductsPage
(src/features/products/index.js is the file shown above.) Now the app shell:
import { Routes, Route, Link } from 'react-router'
import { ProductsPage } from '../features/products'
function AppRoutes() {
return (
<div>
<nav><Link to="/">Home</Link> | <Link to="/products">Products</Link></nav>
<Routes>
<Route path="/" element={<h1>Welcome</h1>} />
<Route path="/products" element={<ProductsPage />} />
</Routes>
</div>
)
}
export default AppRoutes
import { BrowserRouter } from 'react-router'
import AppRoutes from './app/routes.jsx'
function App() {
return (
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
)
}
export default App
When to switch
Do not start a three-page app with five levels of folders. A reasonable path:
- Start flat or grouped by kind (Organizing Components).
- When a folder has too many unrelated files, or work keeps crossing folders, extract the first feature.
- Move features one at a time; the app keeps running throughout.
There is no single correct structure. Pick one, write it down for the team and keep it consistent. Many teams use a mix: feature folders for the product, and a shared folder for the building blocks.
Key points
- Group code by feature so related files live together and changes stay local.
- Give each feature a small public
index.jsand avoid reaching into other features’ internals. - Keep dependencies one-directional:
sharedknows nothing of features, features knowshared,appknows both.