cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

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.

By the end, you can
  • 01
    Choose 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.
  • 02
    Place shared codeDecide where a component, hook, or helper belongs, and keep imports flowing one way: from app, to features, to shared.
  • 03
    Weigh 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.

The tea shop at five filesPop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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.

A window named src holds five files: main.jsx, App.jsx, index.css, ProductList.jsx, and ProductCard.jsx. On the right, four new files wait: CartPanel.jsx, useCart.js, formatPrice.js, and CheckoutForm.jsx, with an arrow pointing at src and the question: where do they go?srcmain.jsxApp.jsxindex.cssProductList.jsxProductCard.jsxwhere dothey go?new filesCartPanel.jsxuseCart.jsformatPrice.jsCheckoutForm.jsx
Five files fit in one folder: you can see them all at once. The shop will grow to 40 files, and each new one needs a home.

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.

Real-life analogyA hardware store, or a plumber's bag

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/ holds Button and formatPrice, which every feature uses
The main idea

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:

The shop at 12 files, sorted by typeText
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.jsx

The 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:

The shop at 24 files, still sorted by typeText
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.jsx

components/ 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:

The shop at 40 files, sorted by featureText
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.jsx

The tree has three top folders, each with one job:

  • app/ wires the whole app together: App.jsx with its providers, routes.jsx with a page for each address (as in the Routing lesson), the Header every page shows, and the global styles.
  • features/ holds one folder per feature. features/cart/ has the cart panel, a cart line, their styles, the useCart hook, and their tests. Each feature folder also has an index.js; the barrels section explains it.
  • shared/ holds code that two or more features use: a Button, a Modal, useDebounce, formatPrice, and the one client.js every request goes through.
  • main.jsx stays at the top of src/, where index.html looks for it.
Top, by type: four crates named components, tests, styles, and api, each holding one part of checkout: the form, its test, its styles, and its request. Bottom, by feature: one crate named features/checkout holding all four parts together.By type: one change, 4 foldersformcomponentstesttestsstylesstylesrequestapiBy feature: the same change, 1 folderformteststylesrequestfeatures/checkout
Adding a discount code changes checkout's form, its test, its styles, and its request. Sorted by type, those four files sit in four folders. Sorted by feature, they sit together in one.

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.

Playground: follow one change through the folders
By type4 folders touched

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/4 files
  • 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/2 files
  • App.jsx
  • global.css
  • main.jsx
  • routes.jsx

Folders: components/, tests/, styles/, api/

By feature1 folder touched

src/

  • app/4 files
  • features/
    • account/7 files
    • cart/7 files
    • checkout/
      • CheckoutForm.css
      • CheckoutForm.jsx
      • CheckoutForm.test.jsx
      • index.js
      • ordersApi.js
    • products/8 files
  • shared/8 files
  • main.jsx

Folders: features/checkout/

Try it yourself
Add a discount code to checkout: 4 folders by type, 1 folder by feature.

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.

The same shop, sorted two ways: 36 files by type (the four index.js files exist only by feature) and 40 by feature. Highlighted files are the ones the change edits or deletes. Folders the change doesn't touch close to one line.

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.

Folders by type or by feature
QuestionBy typeBy 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 /shop to 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.

The shop with feature foldersPop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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.jsx sits next to CartLine.jsx, not in a far-away tests/ folder. Vitest finds files named .test. or .spec. anywhere, so this needs no setup.
  • Styles beside their component. A CartLine.module.css or a CartLine.css lives with CartLine.jsx (the Styling components lesson covers both).
  • A hook beside its only user. If only the cart calls useCart, it lives in features/cart/, not in a global hooks/ 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.

Where shared components, hooks, and helpers live

Some code belongs to no single feature. Button appears in products, the cart, and checkout. formatPrice shows every price. Code like this goes in shared/, sorted inside by type, because that's where type folders shine: small, general pieces that know nothing about the shop.

  • shared/ui/: components with no business logic, like Button and Modal. They take props and draw; they don't fetch, and they don't know what a cart is.
  • shared/hooks/: custom hooks any feature can use, like useDebounce.
  • shared/utils/: pure functions such as formatPrice and validateEmail. No React, no requests: the easiest code in the app to test.
  • shared/api/: the one client.js that every feature's requests go through, with the server's address and error handling in one place.

When does code move to shared? When a second feature needs it, not before. Start a component next to the code that uses it. If checkout later needs the cart's CartLine, that's the moment to decide: move it to shared/ui/ if it's general, or let checkout import it from features/cart's front door. Moving a file costs a few import changes; guessing early fills shared/ with pieces nobody reuses, and hides which feature owns them.

The second rule is about which way imports point. Picture the folders in three layers: app/ on top, features/ in the middle, and shared/ at the bottom.

A card named app at the top, with arrows down to three cards: features/products, features/cart, and features/checkout. Arrows go down from each feature to one card named shared at the bottom. A note says shared never imports from features.appimports point downfeatures/productsfeatures/cartfeatures/checkoutsharedshared never imports from features
Arrows are imports. app/ imports the features, the features import shared/, and nothing points back up. A feature that needs another one imports that feature's index.js.

Each layer imports only from the layers below it. app/ may import any feature. A feature may import shared/, and another feature's front door (its index.js), but never the files inside it. shared/ imports nothing from features: if Button needed the cart, it wouldn't be shared anymore. With the arrows all pointing down, you can change a feature knowing nothing below it depends on it, and circular imports between layers can't happen.

Where does shared state go? A context that only the cart reads, like a CartProvider, lives in features/cart/. The place that puts it around every page is app/App.jsx. A store from a state management library follows the same rule: in the feature that owns the data, or in shared/ if many features write to it.

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:

features/cart/index.js: the cart's front doorJavaScript
// 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.

One helper through a barrelPop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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.

A card App.jsx, which wants one helper, has an arrow to a card utils/index.js. Three arrows lead from it to three cards: formatPrice.js, slugify.js, and validateEmail.js. A note says all three run.App.jsxwants 1 helperutils/index.jsformatPrice.jsslugify.jsvalidateEmail.jsall 3 run
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.js makes a loop, and a loop can run a file before the value it needs exists. Exercise 5 has one that crashes with Cannot 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.push above, can't.

How to read the table: each row is one thing a barrel gives you, next to what it costs.

What a barrel file gives, and what it costs
UseWhat you getWhat it costs
Shorter importsimport { 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 doorThe 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 folderOnly 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:

Relative imports from deep inside a featureJavaScript
// 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.

The same imports with the @ aliasJavaScript
// 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:

vite.config.js: @ means srcJavaScript
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):

jsconfig.json: the same alias, for your editorJSON
{
  "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:

eslint.config.js: no reaching inside a featureJavaScript
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.

Common names for each kind of file
Kind of fileCommon nameRule or convention?
A componentCartLine.jsx, exporting CartLineThe file name is a convention. The component's name must start with a capital letter, or JSX treats it as an HTML tag.
A hookuseCart.js, exporting useCartThe use prefix is a rule: it's how React's linter knows to check the Rules of Hooks.
A helperformatPrice.js, exporting formatPriceA convention: camelCase, named after the one thing it does.
A testCartLine.test.jsx, beside CartLine.jsxA convention your test runner looks for. Vitest finds .test. and .spec. files by default.
StylesCartLine.css or CartLine.module.cssA convention. .module.css is a real signal: Vite and Next.js treat it as a CSS Module.
A folderfeatures/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.

Playground: watch the shop grow
5 filesno folders yet

src/

  • App.jsx
  • index.css
  • main.jsx
  • ProductCard.jsx
  • ProductList.jsx
Step 1 of 45 files: one folder
5 files: one folder

A product list and a card. Every file sits in src/, and you can see them all at once. No folders needed yet.

Each step is the same shop with more features. Files tagged new appeared at this step; the others were there before, maybe in another folder.

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:

Where does this file go?
  • CartLine.jsx, used only by CartPanel
  • Button.jsx, used by products, cart, and checkout
  • ordersApi.js, which sends an order, used only by checkout
  • routes.jsx, which picks a feature's page for each address
  • formatPrice.js, used by products and cart
  • CheckoutForm.test.jsx, the checkout form's test
  • useDebounce.js, used by product search and the checkout address box
  • App.jsx, which puts the cart and account providers around every page
Try it yourself
0 of 8 correct

Sort each file into the folder it belongs in, from who uses it.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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, @/shared looks 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.

Exercise 1 · Warm-upCount the folders

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?

Starter codeText
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.jsx

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFix an import after a move

    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>  );}
    ResultLoading…

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeGive the cart a front door

      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>  );}
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeMove a helper to shared

        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} />    </>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeThe barrel that broke the shop

          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>;
          }

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeStop imports that reach inside a feature

            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?

            Starter codeJavaScript
            export default [
              {
                files: ["src/**/*.{js,jsx}"],
                rules: {
                  "no-restricted-imports": ["error", {
                    patterns: [],
                  }],
                },
              },
            ];

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask who uses the file, and which way the import points.

              The project structure quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Does React tell you how to arrange the folders in your project?

                Choose an answer to see the explanation.

              2. 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.

              3. Question 3 of 7A change touches CheckoutForm.jsx, CheckoutForm.test.jsx, CheckoutForm.css, and ordersApi.js. In folders by type, how many folders is that?

                Choose an answer to see the explanation.

              4. Question 4 of 7useDebounce is used by product search and by the checkout address box. Where should it live?

                Choose an answer to see the explanation.

              5. Question 5 of 7What does the @/ in import Button from "@/shared/ui/Button.jsx" mean?

                Choose an answer to see the explanation.

              6. Question 6 of 7In a Next.js app, what does a folder named _components inside the app folder do?

                Choose an answer to see the explanation.

              7. 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/ to features/ to shared/. A feature uses another only through its index.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's paths, 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.

              CompleteFrontend Clear concepts. Working examples.