EthicsComputerCourses
Join training +91 90532 10052

Project Structure & Tooling

React · Lesson 34 of 41 Advanced 8 min read Updated

A tutorial example fits in one file. A real product, such as a school management system or a shop's admin panel, has hundreds of components written by several people over years. Without agreed rules for where files go and how code looks, every new feature takes longer, and nobody dares to delete anything.

React itself does not prescribe a folder structure; that freedom is why teams must choose one. In this lesson you will compare grouping by file type with grouping by feature, learn common naming rules, set up import aliases, and add the tools every professional project runs: a linter, a formatter and a set of npm scripts. Frameworks such as Next.js decide much of the structure for you; see the Next.js project structure lesson for that case.

Group by feature, not by file type

Small projects often start by grouping files by type: all components in one folder, all hooks in another. It looks tidy at first. But with 200 components, changing "orders" means opening five folders, and you cannot tell which files belong together. Most growing projects group by feature instead:

Text
src/
  app/                    App shell: providers, router, global styles
    App.jsx
    router.jsx
    providers.jsx
  features/
    orders/               Everything about orders lives together
      api.js              fetch functions and query options
      OrdersPage.jsx
      OrderTable.jsx
      OrderTable.test.jsx
      useOrderFilters.js
      OrderTable.module.css
    products/
      api.js
      ProductsPage.jsx
      ProductForm.jsx
    auth/
      AuthProvider.jsx
      LoginPage.jsx
  components/             Shared, feature-free building blocks
    Button.jsx
    Modal.jsx
    DataTable.jsx
  hooks/                  Shared hooks such as useDebouncedValue.js
  lib/                    Plain helpers: formatRupees.js, apiClient.js
  main.jsx

A few rules keep this healthy:

  • Features may import from shared folders (components, hooks, lib), and shared folders never import from features. A Button that knows about orders is no longer shared.
  • Features avoid reaching into each other's files. If products needs something from orders, either move it to a shared folder or import only from a small, deliberate list of exports.
  • Colocate. A component's test, CSS Module and helper hook sit next to it, so deleting a feature means deleting one folder.
  • Do not create folders "just in case". Start flat inside a feature and add sub-folders when a feature grows past ten or so files.

Naming conventions

Agreeing on names matters more than which names you pick. These are the most common choices in React teams:

ItemConventionExample
Component filesPascalCase, same as the componentOrderTable.jsx
HookscamelCase starting with useuseOrderFilters.js
Helpers and API filescamelCaseformatRupees.js, api.js
Testssame name plus .testOrderTable.test.jsx
CSS Modulessame name plus .module.cssOrderTable.module.css
ConstantsUPPER_SNAKE_CASEMAX_UPLOAD_MB

Keep one main component per file. A component file should export only components, because Vite's Fast Refresh (the live update you saw in Set Up React with Vite) can only keep state when a file exports components and nothing else; mixing in other exports makes the page reload instead.

How the pieces of a feature connect

Inside a feature, each file has one job. The API file knows URLs, the hook knows state, and the page component knows layout. The Try-it editor runs a single file, so this example marks where each file would begin:

React JSXTry it yourself
import { useMemo, useState } from "react";
import { createRoot } from "react-dom/client";

// ---- lib/formatRupees.js (shared helper) ----
const formatRupees = (n) => `₹${n.toLocaleString("en-IN")}`;

// ---- features/orders/api.js (would call the server) ----
const orders = [
  { id: "A-101", customer: "Meera", city: "Kochi", amount: 2450, status: "paid" },
  { id: "A-102", customer: "Farhan", city: "Lucknow", amount: 899, status: "pending" },
  { id: "A-103", customer: "Ananya", city: "Pune", amount: 12999, status: "paid" },
];
const getOrders = () => orders;

// ---- features/orders/useOrderFilters.js (state and derived data) ----
function useOrderFilters(list) {
  const [status, setStatus] = useState("all");
  const visible = useMemo(() => list.filter((o) => status === "all" || o.status === status), [list, status]);
  const total = visible.reduce((sum, o) => sum + o.amount, 0);
  return { status, setStatus, visible, total };
}

// ---- features/orders/OrderTable.jsx (presentation only) ----
function OrderTable({ rows }) {
  return (
    <table>
      <thead><tr><th>Order</th><th>Customer</th><th>Amount</th></tr></thead>
      <tbody>
        {rows.map((o) => (
          <tr key={o.id}><td>{o.id}</td><td>{o.customer}, {o.city}</td><td>{formatRupees(o.amount)}</td></tr>
        ))}
      </tbody>
    </table>
  );
}

// ---- features/orders/OrdersPage.jsx (puts it together) ----
function OrdersPage() {
  const { status, setStatus, visible, total } = useOrderFilters(getOrders());
  return (
    <main>
      <h2>Orders</h2>
      <label>
        Status{" "}
        <select value={status} onChange={(e) => setStatus(e.target.value)}>
          <option value="all">All</option>
          <option value="paid">Paid</option>
          <option value="pending">Pending</option>
        </select>
      </label>
      <OrderTable rows={visible} />
      <p>Total: {formatRupees(total)}</p>
    </main>
  );
}

createRoot(document.getElementById("root")).render(<OrdersPage />);

Because OrderTable only receives rows, it can be reused for a customer's order history and tested without any filters. Because useOrderFilters holds no markup, it can be tested with renderHook as shown in Testing Hooks, Context & APIs. That separation is what a good structure buys you.

Import aliases

Deep folders lead to imports such as ../../../components/Button.jsx, which break whenever a file moves. An alias gives the src folder a short name, usually @:

JavaScript
// vite.config.js
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)) },
  },
});

// Anywhere in src:
// import { Button } from "@/components/Button.jsx";

Editors also need to know about the alias for autocomplete, so add the same mapping to jsconfig.json (or tsconfig.json in TypeScript projects) under compilerOptions.paths, as "@/*": ["./src/*"]. In a TypeScript project on Vite 8 you can instead set resolve: { tsconfigPaths: true } and keep the mapping only in tsconfig.json. Vite's performance guide also recommends writing imports with their file extension and avoiding "barrel" files (an index.js that re-exports a whole folder), because they make Vite load every re-exported file even when you need one.

Linting and formatting

A linter reads your code without running it and reports likely bugs. For React, the most valuable checks are the Rules of Hooks and missing effect dependencies. New Vite projects set up Oxlint, a very fast linter, with a .oxlintrc.json file and an npm run lint script. You can add the dependency check to the rules it already contains:

JSON
{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "plugins": ["react", "oxc"],
  "rules": {
    "react/rules-of-hooks": "error",
    "react/exhaustive-deps": "warn",
    "react/only-export-components": ["warn", { "allowConstantExport": true }]
  }
}

Many existing projects use ESLint instead, with the official eslint-plugin-react-hooks. Its recommended preset includes rules-of-hooks, exhaustive-deps and newer rules that find code the React Compiler cannot optimise, such as mutating props or setting state during render. Whichever linter your team uses, run it in the editor and in CI so problems are caught before review.

A formatter such as Prettier decides spacing, quotes and line breaks automatically, so code reviews discuss logic rather than commas:

Terminal
npm install -D prettier
npx prettier --write src

Turn on "format on save" in your editor and add a .prettierrc file only if the team wants to change a default. A typical scripts section then looks like this, and every developer and the build server use the same commands:

JSON
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "oxlint",
    "format": "prettier --write src",
    "test": "vitest"
  }
}

Common mistakes

  • One giant components folder. With hundreds of files nobody can find anything; group by feature.
  • Shared components that import feature code. They stop being reusable and create circular imports.
  • Long relative imports. ../../../ paths break when files move; use an alias.
  • Non-component exports in component files. Fast Refresh then reloads the page and loses state.
  • Ignoring linter warnings. A missing dependency warning is often a real stale-data bug.
  • Arguing about formatting in reviews. Let Prettier decide and move on.

Key points

  • React does not force a structure; agree on one early and write it down.
  • Group by feature: each feature folder holds its pages, components, hooks, API code, styles and tests.
  • Shared folders (components, hooks, lib) never import from features.
  • Use consistent names: PascalCase components, use hooks, .test and .module.css next to the file.
  • An @ alias replaces long relative imports; tell both Vite and the editor about it.
  • Run a linter (Oxlint or ESLint with the React Hooks rules) and a formatter (Prettier) through npm scripts.

Exercise

A small coaching institute's app has these files in one src/components folder: StudentList.jsx, StudentForm.jsx, useStudents.js, FeeReceipt.jsx, useFees.js, Button.jsx, Modal.jsx, formatDate.js and studentsApi.js. Write the feature-based folder tree you would move them into, and state which import rule stops Modal.jsx from importing useFees.js.

Show solution
Text
src/
  features/
    students/
      api.js            (was studentsApi.js)
      StudentList.jsx
      StudentForm.jsx
      useStudents.js
    fees/
      FeeReceipt.jsx
      useFees.js
  components/
    Button.jsx
    Modal.jsx
  lib/
    formatDate.js

Modal.jsx is a shared component, and shared folders must never import from features. If the modal needs fee data, the fees feature should pass it in as props or children, which keeps Modal reusable for students and every other feature.

Quick Quiz

1. What is the main advantage of grouping files by feature?

2. Which import breaks the rules of a feature-based structure?

3. What does an @ import alias solve?

4. Which linter rule catches an effect that uses a value missing from its dependency array?

Learn React with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call