Skip to content
React best practices 4 min read

Project Structure & Conventions

A starter app fits comfortably in a flat src/ folder, but that arrangement collapses the moment the project grows past a dozen screens. The way you lay out files quietly shapes how easy the codebase is to navigate, refactor, and onboard into. This page lays out an opinionated, feature-first structure for React 18/19 apps built with Vite—where related code lives together, shared code is deliberate, and conventions are consistent enough that any file’s purpose is obvious from its location.

Organize by feature, not by file type

The most common starter layout groups files by kind: every component in components/, every hook in hooks/, every helper in utils/. It reads well in a tutorial and falls apart at scale, because a single feature ends up smeared across four folders and unrelated code piles up in each.

Instead, group by feature. A feature is a vertical slice of the product—checkout, authentication, the dashboard—and everything that slice needs lives in one folder.

src/
  features/
    cart/
      components/
        CartPage.jsx
        CartLineItem.jsx
      hooks/
        useCart.js
      api/
        cart.api.js
      cart.test.jsx
      index.js
    auth/
      components/
        LoginForm.jsx
      hooks/
        useAuth.js
      index.js
  shared/
    components/
    hooks/
    lib/
  app/
    App.jsx
    routes.jsx
  main.jsx

When a requirement changes, the edit usually touches one folder rather than five scattered files. New engineers can read a feature top to bottom without a treasure hunt, and deleting a retired feature means deleting one directory.

Colocate everything a unit owns

Colocation is the single most useful rule: keep a thing next to the code that uses it. A component’s styles, tests, stories, and private subcomponents belong beside the component, not in a parallel folder tree mirroring src/.

features/cart/components/
  CartLineItem.jsx
  CartLineItem.module.css
  CartLineItem.test.jsx
  CartLineItem.stories.jsx

The payoff is locality of behavior: when you open CartLineItem.jsx, its tests and styles are right there, and moving or removing the component moves or removes all of it at once.

import styles from "./CartLineItem.module.css";

export function CartLineItem({ item, onRemove }) {
  return (
    <li className={styles.row}>
      <span>{item.name}</span>
      <span>${(item.price * item.qty).toFixed(2)}</span>
      <button type="button" onClick={() => onRemove(item.id)}>
        Remove
      </button>
    </li>
  );
}

A hook, helper, or component used by exactly one feature should live inside that feature. Promote it to shared/ only when a second feature genuinely needs it—not in anticipation.

Draw a clear line between shared and feature code

The shared/ (or common/) layer is for code with no business meaning: a Button, a useDebounce hook, a formatDate helper. Feature code may freely import from shared/, but the dependency must never flow the other way.

LayerContainsMay import fromExample
shared/Generic, reusable, business-agnosticOther shared/ onlyButton, useMediaQuery
features/<x>/One vertical product sliceshared/, same featureCartPage, useCart
app/Wiring: routing, providers, layoutfeatures/, shared/App.jsx, routes.jsx

If shared/ ever needs to import from features/, that code was not actually shared—it belongs in a feature. Keeping the dependency direction one-way (app → features → shared) prevents circular imports and keeps features independently deletable.

Adopt consistent naming conventions

Conventions only help if they are predictable, so pick one rule per category and apply it everywhere:

  • Component files: PascalCase.jsx matching the exported component (LoginForm.jsx exports LoginForm).
  • Hooks: useThing.js, always prefixed with use.
  • Non-component modules: camelCase.js or descriptive suffixes (cart.api.js, date.utils.js).
  • Tests: same name plus .test.jsx; stories plus .stories.jsx.
  • One main export per file, named—reserve default exports for route components that frameworks lazy-load.
// useCart.js — a hook file, prefixed and camelCased after `use`
import { useContext } from "react";
import { CartContext } from "./CartContext";

export function useCart() {
  const ctx = useContext(CartContext);
  if (!ctx) {
    throw new Error("useCart must be used within <CartProvider>");
  }
  return ctx;
}

Use barrel files deliberately

A barrel is an index.js that re-exports a folder’s public surface, so callers import from the feature root rather than reaching into its internals.

// features/cart/index.js
export { CartPage } from "./components/CartPage";
export { useCart } from "./hooks/useCart";
// CartLineItem and CartContext are internal — intentionally not exported.
// Consumers import the public API, not deep paths.
import { CartPage, useCart } from "@/features/cart";

Barrels define an explicit public boundary for each feature, which is their real value. Use them at the feature root, but be sparing inside large shared/ libraries—a single huge barrel can defeat tree-shaking and create import cycles when modules re-import their own barrel.

Pair barrels with a path alias such as @/ (configured in vite.config.js and jsconfig.json/tsconfig.json) so imports read @/features/cart instead of ../../../features/cart.

Best Practices

  • Group source by feature so a change touches one folder, not a file-type sprawl.
  • Colocate components with their tests, styles, stories, and private children.
  • Keep shared/ strictly business-agnostic and the dependency direction one-way.
  • Promote code to shared/ only on the second real use, never preemptively.
  • Apply one consistent naming rule per category—PascalCase components, use-prefixed hooks.
  • Expose each feature through a small barrel index.js and import via a @/ path alias.
  • Reserve default exports for lazy-loaded route components; prefer named exports elsewhere.
Last updated June 14, 2026
Was this helpful?