ReactFolder Structure Best Practices

Folder Structure Best Practices

There is no single correct way to structure a React project. But there are patterns that scale well and patterns that create friction as teams and codebases grow. This page documents the approaches used in successful production applications and the reasoning behind each choice.

Layer-Based vs Feature-Based Structure

The two dominant approaches are organizing files by technical layer (components, hooks, services) or by feature (auth, dashboard, checkout).

Layer-Based

Feature-Based

Organization

By type: /components, /hooks, /services

By domain: /auth, /dashboard, /checkout

Works well at

Small projects, ≤3 developers

Medium to large projects, growing teams

Problem at scale

Changing one feature touches 5+ folders

N/A — related files stay together

Onboarding

Familiar to newcomers

Requires understanding the domain

Co-deletion

Hard — parts of a feature are spread out

Easy — delete one folder to remove a feature

Feature-based structure wins at scale because it mirrors how developers think about work: "I am working on authentication" maps to src/features/auth/, not to simultaneously touching /components/LoginForm, /hooks/useAuth, /services/authService, and /store/authSlice.

Recommended Structure for a Medium-Large App

Text
src/
├── app/                    # Next.js App Router or React Router entry
│   ├── layout.tsx
│   ├── page.tsx
│   └── (routes)/
│
├── features/               # Domain-organized feature modules
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   ├── RegisterForm.tsx
│   │   │   └── index.ts
│   │   ├── hooks/
│   │   │   ├── useAuth.ts
│   │   │   └── useSession.ts
│   │   ├── services/
│   │   │   └── authApi.ts
│   │   ├── store/
│   │   │   └── authSlice.ts
│   │   ├── types/
│   │   │   └── auth.types.ts
│   │   └── index.ts        # public API of the feature
│   │
│   ├── dashboard/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── index.ts
│   │
│   └── checkout/
│       ├── components/
│       ├── hooks/
│       └── index.ts
│
├── shared/                 # Truly reusable, cross-feature code
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.test.tsx
│   │   │   ├── Button.stories.tsx
│   │   │   └── index.ts
│   │   ├── Modal/
│   │   └── index.ts
│   ├── hooks/
│   │   ├── useLocalStorage.ts
│   │   ├── useDebounce.ts
│   │   └── index.ts
│   └── utils/
│       ├── formatDate.ts
│       ├── cn.ts           # className helper
│       └── index.ts
│
├── lib/                    # Third-party wrappers and configuration
│   ├── axios.ts            # configured axios instance
│   ├── queryClient.ts      # React Query / SWR config
│   └── analytics.ts        # analytics SDK wrapper
│
└── types/                  # App-wide TypeScript types
    ├── api.types.ts
    ├── models.ts
    └── env.d.ts
Feature Module: The Index File (Public API)

Each feature directory should have an index.ts that exports only what other features are allowed to import. This creates a clear boundary — other features import from features/auth, not from internal paths:

TS
// src/features/auth/index.ts — the public API of the auth feature

// ✓ Exported — other features may use these
export { LoginForm } from './components/LoginForm'
export { RegisterForm } from './components/RegisterForm'
export { useAuth } from './hooks/useAuth'
export type { User, AuthState } from './types/auth.types'

// ✗ Not exported — internal implementation details
// authReducer, authApi internals, useSessionRefresh (internal hook)

With this pattern, the import in another feature is clean and does not expose internal structure:

TS
// ✓ Import from the feature's public API
import { useAuth } from '@/features/auth'

// ✗ Never import from internal paths of another feature
import { useAuth } from '@/features/auth/hooks/useAuth'
The shared/ Directory

Code in shared/ must have no knowledge of any specific feature. If a component or hook depends on authSlice or checkoutService, it does not belong in shared/. Move it to the feature it belongs to, or create a new feature.

  • shared/components/ — Button, Input, Modal, Tooltip, Spinner, Badge

  • shared/hooks/ — useDebounce, useLocalStorage, useIntersectionObserver, useMediaQuery

  • shared/utils/ — date formatting, string helpers, className utils, validation

  • shared/icons/ — SVG icon components (or re-exports from an icon library)

Co-locating Tests and Stories

Keep tests and Storybook stories next to the component file, not in a separate __tests__ folder. This makes them impossible to orphan when you move a component:

Text
// ✓ Co-located — test and story live with the component
shared/components/Button/
  Button.tsx
  Button.test.tsx       # unit / integration test
  Button.stories.tsx    # Storybook story
  index.ts

// ✗ Separate __tests__ folder — tests become orphaned when components move
__tests__/
  Button.test.tsx       # where's the component? which Button?
The lib/ Directory

Third-party libraries often need configuration or wrapping before use across the app. The lib/ directory holds those wrappers, making it easy to change the underlying library in one place:

TS
// src/lib/http.ts — configured fetch/axios wrapper
import axios from 'axios'

export const http = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  timeout: 10_000,
  headers: { 'Content-Type': 'application/json' },
})

http.interceptors.request.use(config => {
  // attach auth token
  return config
})

// Now every feature imports from lib, not from axios directly:
import { http } from '@/lib/http'
Naming Conventions

Type

Convention

Examples

React component

PascalCase .tsx

UserCard.tsx, LoginForm.tsx

Custom hook

camelCase, prefix use

useAuth.ts, useDebounce.ts

Redux/Zustand slice

camelCase + Slice

authSlice.ts, cartSlice.ts

Utility function

camelCase

formatDate.ts, validateEmail.ts

Type file

camelCase + .types

user.types.ts, api.types.ts

Barrel export

index.ts

index.ts

Test file

same name + .test

Button.test.tsx

Story file

same name + .stories

Button.stories.tsx

Barrel Exports and Import Paths

Barrel files (index.ts) let consumers import from a short path instead of a full internal path:

TS
// src/shared/components/index.ts
export { Button } from './Button'
export { Modal } from './Modal'
export { Spinner } from './Spinner'
export { Input } from './Input'

// Consumer — short, readable import
import { Button, Modal, Spinner } from '@/shared/components'

// Without barrel — verbose and fragile to file moves
import { Button } from '@/shared/components/Button/Button'
import { Modal } from '@/shared/components/Modal/Modal'
Warning
Deep barrel re-exports (barrels that re-export from other barrels) can cause slow startup in development with some bundlers. If `npm run dev` becomes slow, flatten your barrel files or switch to direct imports in critical paths.
Tip
Start simpler than you think you need. A new project can begin with a flat `src/components/` and `src/hooks/` structure. Migrate to feature folders when you feel the pain of unrelated files sharing a directory. The right time to reorganize is when navigation and code ownership become confusing — not at project kickoff.