Structuring a React project
Organize a growing React app: folders by type or by feature, colocation, shared code, barrel files, import aliases, and names, with live examples.
- 01Choose a folder layoutCompare folders by type with folders by feature, and move a growing app from one to the other when it starts to hurt.
- 02Place shared codeDecide where a component, hook, or helper belongs, and keep imports flowing one way: from app, to features, to shared.
- 03Weigh barrels, aliases, and namesUse index.js files, an @/ alias, lint rules, and naming conventions where they help, and know what each one costs.
A shop that outgrew one folder
Every React app starts small. Here is a tea shop after its first evening of work: a list of products, a card for each one, and a count of what's in the cart. The result is live: click Add to cart, and open the tabs to read each file.
import { useState } from "react";import ProductList from "./ProductList.jsx"; const products = [ { id: 1, name: "Tea", price: 3 }, { id: 2, name: "Cake", price: 4 },]; export default function App() { const [count, setCount] = useState(0); return ( <main> <p>Items in cart: {count}</p> <ProductList products={products} onAdd={() => setCount(count + 1)} /> </main> );}In App.jsx, line 2 imports ProductList from its own file, lines 4 to 7 are the products, and line 15 hands them to the list with a function that adds one to the count. ProductList.jsx imports ProductCard.jsx on its line 1 and draws one card per product. index.css styles the cards. The preview adds the fifth file, main.jsx, which every new Vite project has: it renders App into the page (Setting up a React project walks through it).
Five files sit in one folder, src/, and you can see them all at once. Now the shop needs a cart panel, a checkout form, an account page, requests to a server, and tests. By the end of this lesson it has 40 files. Each one needs a home, and where you put them decides how easy the next change is.
The first thing to know: React doesn't care where your files are. It never reads a folder name. Your build tool follows the import lines from main.jsx and bundles whatever they reach, from any folder. React's older documentation said it plainly: "React doesn't have opinions on how you put files into folders." The current docs at react.dev don't prescribe a layout either. Next.js calls itself "unopinionated" about how you organize your files, beyond the few file names its router reads.
So there's no official structure to learn. There are a few layouts that teams keep arriving at, two ideas that make any layout work (keep files that change together close, and make imports flow one way), and some tools with real costs. That's this lesson.
A hardware store sorts its stock by what each thing is, which works when you browse. A plumber on a job wants everything for that one job in one bag, so nothing is left behind. Tools every job needs stay on a shelf all the bags share.
- In real life: A store puts all hammers in one aisle, all pipes in another
- In React: Folders by type:
components/,hooks/,api/ - In real life: A plumber packs one bag with everything for fixing a sink
- In React: A feature folder:
features/cart/holds the cart's components, hooks, and tests - In real life: To fix a sink, you grab one bag
- In React: To change the cart, you open one folder
- In real life: A tape measure every job needs stays on the shared shelf
- In React:
shared/holdsButtonandformatPrice, which every feature uses
A React project's structure is a team's choice, not React's. A good one keeps the files that change together close to each other (colocation), gives each part of the app its own folder as it grows, and lets imports point one way: from the app, to its features, to shared code.
Folders by type: components, hooks, utils
The first move most projects make is to sort files by what they are. Components go in components/, custom hooks in hooks/, small helper functions in utils/, and code that talks to a server in api/. Here is the shop at 12 files, after the cart arrived:
src/
├── api/
│ └── products.js
├── components/
│ ├── CartLine.jsx
│ ├── CartPanel.jsx
│ ├── Header.jsx
│ ├── ProductCard.jsx
│ └── ProductList.jsx
├── hooks/
│ ├── useCart.js
│ └── useProducts.js
├── utils/
│ └── formatPrice.js
├── App.jsx
├── index.css
└── main.jsxThe tree lists folders first and files A to Z, like a code editor's file list. Four folders, and each is easy to name: Header.jsx and CartPanel.jsx are components, useCart.js is a hook, formatPrice.js is a helper that turns 3 into $3.00, and products.js fetches the product list. At this size, sorting by type works well. You never wonder where a hook goes.
Keep going. Checkout, an account, a few shared pieces, and the first tests bring the shop to 24 files:
src/
├── api/
│ ├── auth.js
│ ├── orders.js
│ └── products.js
├── components/
│ ├── Button.jsx
│ ├── CartLine.jsx
│ ├── CartPanel.jsx
│ ├── CheckoutForm.jsx
│ ├── Header.jsx
│ ├── LoginForm.jsx
│ ├── ProductCard.jsx
│ ├── ProductFilters.jsx
│ ├── ProductList.jsx
│ └── ProfilePage.jsx
├── hooks/
│ ├── useAuth.js
│ ├── useCart.js
│ ├── useDebounce.js
│ └── useProducts.js
├── tests/
│ ├── CartPanel.test.jsx
│ └── CheckoutForm.test.jsx
├── utils/
│ ├── formatPrice.js
│ └── validateEmail.js
├── App.jsx
├── index.css
└── main.jsxcomponents/ now holds 10 files: the pieces of four different features (products, cart, checkout, account) mixed with Button and Header, which everyone uses. Nothing in the folder says which file belongs to which feature. A change to checkout means opening components/CheckoutForm.jsx, api/orders.js, and tests/CheckoutForm.test.jsx: three folders for one change.
That's the cost of folders by type. They group files that look alike, but files that look alike rarely change together. The files that do change together, a form, its request, and its test, end up far apart, and the distance grows with every feature.
Folders by feature: one folder per part of the app
The other common layout sorts files by what they're for. Each feature, a part of the app a user would name, like the cart or checkout, gets a folder holding everything it needs: components, styles, hooks, requests, and tests. Here is the shop at 40 files, sorted that way:
src/
├── app/
│ ├── App.jsx
│ ├── global.css
│ ├── Header.jsx
│ └── routes.jsx
├── features/
│ ├── account/
│ │ ├── authApi.js
│ │ ├── index.js
│ │ ├── LoginForm.jsx
│ │ ├── LoginForm.test.jsx
│ │ ├── OrderHistory.jsx
│ │ ├── ProfilePage.jsx
│ │ └── useAuth.js
│ ├── cart/
│ │ ├── CartLine.css
│ │ ├── CartLine.jsx
│ │ ├── CartPanel.jsx
│ │ ├── CartPanel.test.jsx
│ │ ├── index.js
│ │ ├── useCart.js
│ │ └── useCart.test.js
│ ├── checkout/
│ │ ├── CheckoutForm.css
│ │ ├── CheckoutForm.jsx
│ │ ├── CheckoutForm.test.jsx
│ │ ├── index.js
│ │ └── ordersApi.js
│ └── products/
│ ├── index.js
│ ├── ProductCard.css
│ ├── ProductCard.jsx
│ ├── ProductFilters.jsx
│ ├── ProductList.jsx
│ ├── ProductList.test.jsx
│ ├── productsApi.js
│ └── useProducts.js
├── shared/
│ ├── api/
│ │ └── client.js
│ ├── hooks/
│ │ └── useDebounce.js
│ ├── ui/
│ │ ├── Button.css
│ │ ├── Button.jsx
│ │ └── Modal.jsx
│ └── utils/
│ ├── formatPrice.js
│ ├── formatPrice.test.js
│ └── validateEmail.js
└── main.jsxThe tree has three top folders, each with one job:
app/wires the whole app together:App.jsxwith its providers,routes.jsxwith a page for each address (as in the Routing lesson), theHeaderevery page shows, and the global styles.features/holds one folder per feature.features/cart/has the cart panel, a cart line, their styles, theuseCarthook, and their tests. Each feature folder also has anindex.js; the barrels section explains it.shared/holds code that two or more features use: aButton, aModal,useDebounce,formatPrice, and the oneclient.jsevery request goes through.main.jsxstays at the top ofsrc/, whereindex.htmllooks for it.
The difference shows when something changes. The playground below holds the same 40-file shop sorted both ways. Pick a change: each side highlights the files it touches, and closes the folders it doesn't.
4 folders touchedsrc/
api/authApi.jsclient.jsordersApi.jsproductsApi.js
components/Button.jsxCartLine.jsxCartPanel.jsxCheckoutForm.jsxHeader.jsxLoginForm.jsxModal.jsxOrderHistory.jsxProductCard.jsxProductFilters.jsxProductList.jsxProfilePage.jsx
hooks/4 filesstyles/Button.cssCartLine.cssCheckoutForm.cssProductCard.css
tests/CartPanel.test.jsxCheckoutForm.test.jsxformatPrice.test.jsLoginForm.test.jsxProductList.test.jsxuseCart.test.js
utils/2 filesApp.jsxglobal.cssmain.jsxroutes.jsx
Folders: components/, tests/, styles/, api/
1 folder touchedsrc/
app/4 filesfeatures/account/7 filescart/7 filescheckout/CheckoutForm.cssCheckoutForm.jsxCheckoutForm.test.jsxindex.jsordersApi.js
products/8 files
shared/8 filesmain.jsx
Folders: features/checkout/
A new field, its style, its test, and a new value in the order the server gets. By type, those four files sit in four folders. By feature, they sit together in features/checkout.
Add a discount code to checkout touches 4 folders by type and 1 by feature. Remove the account feature is the clearest: by feature, you delete one folder and take its pages out of app/routes.jsx. By type, you hunt through four folders for six files, and a forgotten one stays behind for years. Change how every price looks is the one change that's close in both: the helper is shared code either way.
How to read the table: each row asks one question, with the answer for each layout.
| Question | By type | By feature |
|---|---|---|
| Where does a new file go? | In the folder for what it is: components/, hooks/, api/. | In the folder for what it's for: features/cart/, features/checkout/. |
| How many folders does one change touch? | Often three or four, one per kind of file. | Usually one: the feature's folder. |
| How do you remove a feature? | Find its files in every folder, one by one. | Delete its folder, then fix the imports the build reports. |
| Where does it work well? | Small apps, up to a few dozen files. | Apps with several features, and teams where each person owns a few. |
Most real apps end up mixing the two: feature folders at the top, and inside shared/, small folders by type (ui/, hooks/, utils/). That's exactly the 40-file tree above. React's older documentation said the same: big projects tend to use a mix of both, so the choice you make on day one matters less than it seems.
What Next.js adds
A framework gives some folders a meaning. In a Next.js app, folders inside app/ are addresses: app/cart/page.js is the page at /cart. Only a page.js or route.js file makes an address public, so you can keep a route's own components and tests right beside its page, and Next.js won't serve them. Three conventions help:
- A folder starting with an underscore, like
app/cart/_components/, is a private folder: Next.js leaves it out of routing entirely. - A folder in brackets, like
app/(shop)/, is a route group: it groups pages without adding/shopto their addresses. - An optional
src/folder keeps your app code apart from the config files at the top of the project.
Next.js's own docs show three ways to arrange the rest (code outside app/, in top folders inside it, or split by feature or route) and suggest picking one and using it everywhere. A Vite app has no routing folders: the router is code you write, so the whole src/ folder is yours to arrange.
Keep a component's files side by side
Colocation means keeping files that change together in the same place. React's older docs put it as a rule of thumb: keep files that often change together close to each other. A feature folder is colocation at the size of a feature. It works at the size of one component too: CartLine.jsx, CartLine.css, and CartLine.test.jsx sit side by side, so when the line changes, its style and its test are one click away.
Here is the shop sorted by feature, as a real project with real folders. Open the tabs: each one is named with its path from src/. Click Add on Tea and Cake, then Empty the cart.
import ProductList from "./features/products/ProductList.jsx";import CartPanel from "./features/cart/CartPanel.jsx";import { useCart } from "./features/cart/useCart.js"; const products = [ { id: 1, name: "Tea", price: 3 }, { id: 2, name: "Cake", price: 4 },]; export default function App() { const cart = useCart(); return ( <main> <ProductList products={products} onAdd={cart.add} /> <CartPanel cart={cart} /> </main> );}Lines 1 to 3 of App.jsx reach into two feature folders. Line 11 calls useCart, the hook that keeps the cart's lines, and lines 15 and 16 give the cart to both features. In features/cart/CartPanel.jsx, lines 1 and 2 climb out of the feature with ../../ to reach shared/ui/Button.jsx and shared/utils/formatPrice.js, and line 8 shows the total as $7.00 after Tea and Cake. features/cart/useCart.js keeps the hook beside the panel that uses it.
Press Edit & run and open shared/utils/formatPrice.js. Change "$" to "£". The product prices and the cart total change together, because both features import that one file.
Three habits come from colocation:
- Tests beside their file.
CartLine.test.jsxsits next toCartLine.jsx, not in a far-awaytests/folder. Vitest finds files named.test.or.spec.anywhere, so this needs no setup. - Styles beside their component. A
CartLine.module.cssor aCartLine.csslives withCartLine.jsx(the Styling components lesson covers both). - A hook beside its only user. If only the cart calls
useCart, it lives infeatures/cart/, not in a globalhooks/folder.
Some teams go one step further and give every component its own folder: CartLine/CartLine.jsx, CartLine/CartLine.css, CartLine/CartLine.test.jsx. That's fine for big components. For small ones it doubles the folders and adds little, so most apps keep a feature's files flat until a folder gets long.
Barrel files: a front door, and what it costs
A barrel file is an index.js that does nothing but re-export what other files in its folder export. Here is the cart's:
// src/features/cart/index.js: what the rest of the app may use
export { default as CartPanel } from "./CartPanel.jsx";
export { useCart } from "./useCart.js";Line 2 takes CartPanel's default export and gives it a name; line 3 passes useCart through (the import & export lesson covers export … from). Now the rest of the app writes import { CartPanel, useCart } from "./features/cart". Build tools like Vite look for an index.js when an import names a folder, so ./features/cart reaches it. CartLine.jsx isn't in the barrel, so it stays private to the cart.
That's the good side: a barrel is a feature's public front door. Barrels have a cost too. Importing one name from a barrel loads every file the barrel names, because loading a module runs its top-level code. This example makes that visible: each helper adds its own file name to a list when it runs.
import { formatPrice } from "./shared/utils";import { loaded } from "./shared/loaded.js"; export default function App() { return ( <> <p>Tea costs {formatPrice(3)}</p> <p>Files that ran: {loaded.join(", ")}</p> </> );}Line 1 imports only formatPrice, through shared/utils/index.js. Open that tab: its three lines name three files. Open shared/utils/slugify.js: line 1 imports the list from shared/loaded.js, and line 3 adds "slugify.js" to it. Line 8 of App.jsx shows the list: formatPrice.js, slugify.js, validateEmail.js. Three files ran to give you one function.
Now press Edit & run and change line 1 to import { formatPrice } from "./shared/utils/formatPrice.js". The list shrinks to formatPrice.js.
App.jsx needs only formatPrice. The barrel names three files, so all three load and run. A direct import of formatPrice.js loads one.Three helpers cost nothing. In a real app, the costs add up in three places:
- Slower dev servers and tests. Vite serves each file separately while you work, so a page that imports one name from a big barrel makes the browser fetch every file behind it. Vite's performance guide says to avoid barrel files for this reason. A test file that imports one component through a barrel loads the whole folder too.
- Circular imports. A file inside a folder that imports its own folder's
index.jsmakes a loop, and a loop can run a file before the value it needs exists. Exercise 5 has one that crashes withCannot access 'currency' before initialization. - Bigger bundles, sometimes. For the production build, bundlers drop exports nobody uses (tree shaking), but only when the files have no side effects that must run. A barrel of plain functions usually shakes out fine; one with side effects, like the
loaded.pushabove, can't.
How to read the table: each row is one thing a barrel gives you, next to what it costs.
| Use | What you get | What it costs |
|---|---|---|
| Shorter imports | import { CartPanel, useCart } from "./features/cart" instead of two deep paths. | Every file the barrel names runs, even the ones you didn't import. |
| A public front door | The feature decides what others may use. Its other files stay private. | A file inside the folder that imports its own barrel can start a circular import. |
| Moving files inside a folder | Only index.js changes; files that import the folder don't. | Dev servers and test runners load more files, so they start slower. |
A common middle ground: one small barrel per feature, naming only what other features may use, and no barrels inside shared/ (import shared/ui/Button.jsx directly). Packages with hundreds of exports have the same problem at a bigger size; Next.js's optimizePackageImports setting rewrites imports from such packages so only the modules you use load.
Shorter imports, and rules that guard them
Deep folders make long relative paths. Here is the top of the checkout form, three folders down:
// src/features/checkout/CheckoutForm.jsx
import Button from "../../shared/ui/Button.jsx";
import { formatPrice } from "../../shared/utils/formatPrice.js";
import { useCart } from "../cart";Lines 2 and 3 climb out with ../../, and line 4 reaches the cart's front door. Move CheckoutForm.jsx one folder deeper, and every ../ is wrong. An alias fixes this: a short name, usually @, that always means src/, whatever file you're in.
// src/features/checkout/CheckoutForm.jsx
import Button from "@/shared/ui/Button.jsx";
import { formatPrice } from "@/shared/utils/formatPrice.js";
import { useCart } from "@/features/cart";An alias isn't JavaScript, and React never sees it. Your build tool turns @/shared/ui/Button.jsx into the real path before the code runs, so you set it up there. In Vite, it's resolve.alias:
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});Line 9 maps @ to the src folder next to the config file. fileURLToPath(new URL(…, import.meta.url)) on line 9 builds that folder's full path; Vite's docs say to give an alias a full path, not a relative one. Your editor needs to know the alias too, or its "go to file" and auto-import won't follow it. That's the paths option in jsconfig.json (tsconfig.json in a TypeScript project):
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}Line 4 says that @/ followed by anything means ./src/ followed by the same thing. In a TypeScript project, Vite 8 can read these paths itself with resolve.tsconfigPaths: true, so you write the alias once. Next.js reads paths from tsconfig.json or jsconfig.json on its own, and create-next-app sets up @/* for you by default. (The module resolution lesson shows how tools turn an import into a file.)
A rule that keeps imports pointing down
"Import a feature only through its index.js" is easy to say and easy to forget. A linter can check it on every save. ESLint's built-in no-restricted-imports rule reports imports that match a pattern:
export default [
{
files: ["src/**/*.{js,jsx}"],
rules: {
"no-restricted-imports": ["error", {
patterns: [{
group: ["@/features/*/*"],
message: "Import a feature through its index.js, like @/features/cart.",
}],
}],
},
},
];Line 3 applies the rule to the app's files. Line 7's pattern, @/features/*/*, matches a path that goes into a feature's folder and then names a file in it, and line 8 is the message the linter shows. @/features/cart is allowed; @/features/cart/CartLine.jsx is reported. Files inside the cart still import each other with ./CartLine.jsx, which the pattern doesn't match. Plugins like eslint-plugin-boundaries can check whole layer rules, such as "shared never imports features", if a pattern isn't enough.
Names that say what a file holds
Good names let you find a file without opening it. Most of what follows is convention, but two parts are React's own rules: a component's name starts with a capital letter, because JSX treats <cartLine /> as an HTML tag (Your first component explains why), and a hook's name starts with use, because that's how React's linter knows to check the Rules of Hooks.
How to read the table: each row is one kind of file, a common name for it, and whether that name is a rule or a habit.
| Kind of file | Common name | Rule or convention? |
|---|---|---|
| A component | CartLine.jsx, exporting CartLine | The file name is a convention. The component's name must start with a capital letter, or JSX treats it as an HTML tag. |
| A hook | useCart.js, exporting useCart | The use prefix is a rule: it's how React's linter knows to check the Rules of Hooks. |
| A helper | formatPrice.js, exporting formatPrice | A convention: camelCase, named after the one thing it does. |
| A test | CartLine.test.jsx, beside CartLine.jsx | A convention your test runner looks for. Vitest finds .test. and .spec. files by default. |
| Styles | CartLine.css or CartLine.module.css | A convention. .module.css is a real signal: Vite and Next.js treat it as a CSS Module. |
| A folder | features/order-history/ | A convention: lowercase, with hyphens. macOS and Windows ignore letter case in names and Linux doesn't, so one plain style avoids imports that work on a laptop and break on a server. |
Name a file after the one thing it exports: CartLine.jsx exports CartLine. Then search finds it, and an import line tells you which file to open. A small helper component used only inside that file can stay there. Whether you export with export default or a named export function is up to the team; named exports keep the same name everywhere they're imported, which makes renames and searches easier, so many teams prefer them for everything but pages.
The shop from 5 files to 40
Here is the whole story in one place. Step through the shop's growth: at each step, the tree shows every file, with the new ones tagged.
no folders yetsrc/
App.jsxindex.cssmain.jsxProductCard.jsxProductList.jsx
A product list and a card. Every file sits in src/, and you can see them all at once. No folders needed yet.
Notice what didn't happen: nobody planned the 40-file tree on day one. At 5 files, one folder was right. At 12, type folders were right. Around 24, when one change started to reach into three folders, it was time to move to feature folders. Moving files in a code editor updates their imports for you, so a layout change is an afternoon's work, not a rewrite. React's older docs advised against spending more than five minutes choosing a structure at the start; you learn the right one from the code you write.
For the 40-file shop, practice the decisions the tree is made of:
CartLine.jsx, used only byCartPanelButton.jsx, used by products, cart, and checkoutordersApi.js, which sends an order, used only by checkoutroutes.jsx, which picks a feature's page for each addressformatPrice.js, used by products and cartCheckoutForm.test.jsx, the checkout form's testuseDebounce.js, used by product search and the checkout address boxApp.jsx, which puts the cart and account providers around every page
Sort each file into the folder it belongs in, from who uses it.
Common mix-ups
- "React has an official folder structure." No. React never reads folder names. Frameworks give meaning to a few files, like Next.js's
page.js; the rest is your team's choice. - "Every component needs its own folder with an index.js." No. That doubles the files and the barrels. Flat files in a feature folder are fine until the folder gets long.
- "Anything reusable belongs in shared/ from the start." No. Start code next to its only user, and move it when a second feature needs it.
- "Importing one name from a barrel loads only that name." No. Every file the barrel names loads and runs. The barrel example showed three files running for one function.
- "The @/ alias is a React feature." No. It's set in your build tool and your editor's config. Without that,
@/sharedlooks like a package name and the build fails. - "Tests belong in a separate tests folder." They can, but beside the file they test, they're easier to find and to keep in step. Vitest finds them either way.
Practice exercises
Most exercises are live projects with folders: open the tabs to see every file, press Edit & run, and the result updates as you type. Try a guess first, then use the hints.
This is the 40-file shop sorted by type. You change the login form's error message, and its test. How many folders do you open?
src/
├── api/
│ ├── authApi.js
│ ├── client.js
│ ├── ordersApi.js
│ └── productsApi.js
├── components/
│ ├── Button.jsx
│ ├── CartLine.jsx
│ ├── CartPanel.jsx
│ ├── CheckoutForm.jsx
│ ├── Header.jsx
│ ├── LoginForm.jsx
│ ├── Modal.jsx
│ ├── OrderHistory.jsx
│ ├── ProductCard.jsx
│ ├── ProductFilters.jsx
│ ├── ProductList.jsx
│ └── ProfilePage.jsx
├── hooks/
│ ├── useAuth.js
│ ├── useCart.js
│ ├── useDebounce.js
│ └── useProducts.js
├── styles/
│ ├── Button.css
│ ├── CartLine.css
│ ├── CheckoutForm.css
│ └── ProductCard.css
├── tests/
│ ├── CartPanel.test.jsx
│ ├── CheckoutForm.test.jsx
│ ├── formatPrice.test.js
│ ├── LoginForm.test.jsx
│ ├── ProductList.test.jsx
│ └── useCart.test.js
├── utils/
│ ├── formatPrice.js
│ └── validateEmail.js
├── App.jsx
├── global.css
├── main.jsx
└── routes.jsxcomponents/LoginForm.jsx
tests/LoginForm.test.jsxThe form sits in components/ and its test in tests/: 2 folders. Sorted by feature, both are in features/account/: 1 folder.
Someone moved ProductCard.jsx into features/products/, and the shop stopped building. Fix line 1. What does the card show once it works?
import ProductCard from "./ProductCard.jsx"; export default function App() { return ( <ul className="product-list"> <ProductCard name="Tea" price={3} /> </ul> );}import ProductCard from "./features/products/ProductCard.jsx"; export default function App() { return ( <ul className="product-list"> <ProductCard name="Tea" price={3} /> </ul> );}The card moved to features/products/, so line 1 must name the new path. A code editor updates imports like this for you when you move a file from its file list.
App.jsx reaches into the cart's files. Press Edit & run, fill in the features/cart/index.js tab so it re-exports CartPanel and useCart, and import the cart through it. What one line replaces lines 1 and 2?
import CartPanel from "./features/cart/CartPanel.jsx";import { useCart } from "./features/cart/useCart.js"; export default function App() { const cart = useCart(); return ( <main> <button onClick={() => cart.add({ name: "Tea", price: 3 })}>Add tea</button> <CartPanel cart={cart} /> </main> );}import { CartPanel, useCart } from "./features/cart"; export default function App() { const cart = useCart(); return ( <main> <button onClick={() => cart.add({ name: "Tea", price: 3 })}>Add tea</button> <CartPanel cart={cart} /> </main> );}The new features/cart/index.js re-exports the two things the app may use, and App.jsx imports both from the folder in one line. The cart can now rename or split its files without App.jsx changing.
Products and cart each have their own formatPrice.js, and the shop shows two different prices for the same tea. Move one copy to shared/utils/ and use it in both. What does the cart line show then?
import ProductCard from "./features/products/ProductCard.jsx";import CartLine from "./features/cart/CartLine.jsx"; export default function App() { return ( <> <ProductCard name="Tea" price={3} /> <CartLine name="Tea" price={3} /> </> );}import { formatPrice } from "../../shared/utils/formatPrice.js"; export default function CartLine({ name, price }) { return <p>Cart: {name} {formatPrice(price)}</p>;}Both features now import one formatPrice from shared/utils/, so the shelf and the cart show $3.00. Two copies had drifted apart: the cart's printed 3 USD. Promote code to shared when a second feature needs it, and delete the copies.
The shop crashes with Cannot access 'currency' before initialization, although currency.js clearly sets it. Press Edit & run to see the crash and the three files in shared/utils/. Find the loop and fix it with a one-word change. What does the page show after your fix?
import { formatPrice } from "./shared/utils";
export default function App() {
return <p>Tea: {formatPrice(3)}</p>;
}import { currency } from "./currency.js"; const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency }); export function formatPrice(price) { return formatter.format(price);}App.jsx loads index.js, whose line 1 loads formatPrice.js first. formatPrice.js imports index.js, which is already loading, so nothing new runs, and line 3 reads currency before currency.js has run: "Cannot access 'currency' before initialization". Importing from ./currency.js makes currency.js run first. Files inside a folder shouldn't import their own barrel.
Fill in patterns so ESLint reports any import that reaches past a feature's index.js. Then check it against these three imports: import { CartPanel } from "@/features/cart";, import CartLine from "@/features/cart/CartLine.jsx";, and import Button from "@/shared/ui/Button.jsx";. Which one does your rule report?
export default [
{
files: ["src/**/*.{js,jsx}"],
rules: {
"no-restricted-imports": ["error", {
patterns: [],
}],
},
},
];export default [
{
files: ["src/**/*.{js,jsx}"],
rules: {
"no-restricted-imports": ["error", {
patterns: [{
group: ["@/features/*/*"],
message: "Import a feature through its index.js, like @/features/cart.",
}],
}],
},
},
];The pattern @/features/*/* reports import CartLine from "@/features/cart/CartLine.jsx" and allows @/features/cart and @/shared/ui/Button.jsx. The lesson's test runs ESLint with this rule on all three imports.
Check your understanding
For each question, ask who uses the file, and which way the import points.
Question 1 of 7Does React tell you how to arrange the folders in your project?
Choose an answer to see the explanation.
Question 2 of 7App.jsx imports one helper through this barrel. Which helper files run when the app starts?
Read the code, then predictJavaScript// shared/utils/index.js export { formatPrice } from "./formatPrice.js"; export { slugify } from "./slugify.js"; export { validateEmail } from "./validateEmail.js"; // App.jsx import { formatPrice } from "./shared/utils";Choose an answer to see the explanation.
Question 3 of 7A change touches
CheckoutForm.jsx,CheckoutForm.test.jsx,CheckoutForm.css, andordersApi.js. In folders by type, how many folders is that?Choose an answer to see the explanation.
Question 4 of 7
useDebounceis used by product search and by the checkout address box. Where should it live?Choose an answer to see the explanation.
Question 5 of 7What does the
@/inimport Button from "@/shared/ui/Button.jsx"mean?Choose an answer to see the explanation.
Question 6 of 7In a Next.js app, what does a folder named
_componentsinside theappfolder do?Choose an answer to see the explanation.
Question 7 of 7When should a component move from a feature's folder to shared/?
Choose an answer to see the explanation.
Key takeaways
- React has no official folder structure. It follows imports, not folders; frameworks like Next.js give meaning to only a few file names.
- Folders by type (components, hooks, utils) suit small apps. Folders by feature keep the files that change together in one place, and suit apps with several features.
- Colocate: a component's styles, test, and hook sit beside it. Move code to
shared/when a second feature needs it. - Let imports point one way:
app/tofeatures/toshared/. A feature uses another only through itsindex.js, and a lint rule can enforce that. - A barrel file is a feature's front door, but it loads every file it names; keep barrels small, and never import your own folder's barrel.
- An
@/alias shortens imports. It lives in your build tool's config and your editor'spaths, not in React.
Remember the one-liner.
Keep together what changes together, and let imports point one way.
Coming next: Testing React components. It tests the shop's components the way a user uses them, with Vitest and React Testing Library.