| |

React 4 ⚛️ Understanding Project Directory Structure

A React project is not just a folder of JavaScript files. It is a structure with defined roles: the entry point, the root component, the assets, the configuration, and the build output. Knowing where each piece lives — and why — is the difference between a project you can navigate and one you spend hours searching through. This chapter walks through the directory structure that Vite scaffolds for a React project and explains what every file and folder is for.

The previous chapter covered creating a project with Vite. This chapter assumes the project exists and focuses on understanding what was created. The structure is intentionally minimal: Vite does not add files you do not need. Every file in the scaffolded project is there for a reason.

Key point: The index.html file is the entry point of a Vite project, and it lives at the root of the project — not inside public/ as it did in Create React App. Vite treats index.html as part of the module graph. The <script type="module"> tag inside it points to src/main.jsx, which mounts the React application. The browser requests index.html first, then follows the script tag to the application code.


Why the Directory Structure Matters

A project structure is a set of conventions. When every React project follows the same basic layout, a developer who joins a new project knows where to look. The conventions are not arbitrary — each file and folder has a specific role in the build and runtime lifecycle.

The entry point problem. The browser needs an HTML file to start. That file must reference the JavaScript entry point. In Vite, that file is index.html. It is the first thing the browser requests. Everything else flows from it.

The module graph problem. Vite builds a module graph starting from index.html. The <script type="module" src="/src/main.jsx"> tag is the first edge. From there, Vite follows the imports in main.jsx, then the imports in the files it imports, and so on. The graph is the application.

The static asset problem. Some assets are processed by Vite — images referenced in components, CSS imported by JavaScript. Others are served as-is — a favicon, a robots.txt, a manifest. The two categories live in different folders. Mixing them causes confusion.

The configuration problem. The build tool, the compiler, and the linter each have their own configuration files. They live at the project root. They are not part of the application code, but they control how the application code is built.

The trade-off. The minimal structure is easy to understand but grows as the application grows. A small project has a few files. A large project has components, hooks, utilities, services, and tests in organized folders. The structure evolves with the project.


a. The Root Directory

The root of a Vite React project contains the files that define the project itself. These files are not part of the application’s runtime. They are the configuration and the entry point.

my-react-app/
├── node_modules/
├── public/
├── src/
├── .gitignore
├── index.html
├── package.json
├── package-lock.json
├── vite.config.js
├── eslint.config.js
└── README.md

index.html is the entry point. It is the first file the browser requests. It contains the <div id="root"> where React mounts, and the <script type="module"> tag that loads the application. It lives at the root because Vite treats it as part of the module graph, not as a static asset.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

package.json is the project manifest. It lists the dependencies, the scripts, and the metadata. It is the file the package manager reads and the file the developer edits to add dependencies or scripts.

package-lock.json is the lockfile. It records the exact version of every dependency, including transitive ones. It is generated by the package manager and should not be edited by hand. It must be committed to version control.

vite.config.js is the Vite configuration. It defines the plugins, the path aliases, the proxy settings, and the build options. A minimal React project has one plugin:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
})

eslint.config.js is the ESLint configuration. It defines the linting rules. The scaffolded project includes a basic configuration that enables the recommended rules for React.

.gitignore lists the files and folders that Git should not track. The default file ignores node_modules/, the build output, and the environment files.

README.md is the project documentation. Vite scaffolds a basic README with instructions for the development workflow.


b. The public/ Directory

The public/ directory holds static assets that Vite does not process. Files in public/ are served as-is and can be referenced with an absolute path.

public/
└── vite.svg

The vite.svg file is the favicon. It is referenced in index.html with <link rel="icon" href="/vite.svg" />. The leading slash means the file is served from the root of the deployed application.

The key difference between public/ and src/assets/ is the processing. A file in public/ is copied to the build output as-is. A file in src/assets/ is processed by Vite: images are optimized, small files are inlined as base64, and the file name includes a content hash.

The rule is simple: assets that must keep their exact file name and path go in public/. Assets that can be processed and hashed go in src/assets/. A favicon, a robots.txt, a manifest.json, and a verification file for a third-party service all go in public/.


c. The src/ Directory

The src/ directory contains the application code. This is where the React components live. Vite processes every file in this directory.

src/
├── assets/
│   └── react.svg
├── App.css
├── App.jsx
├── index.css
└── main.jsx

main.jsx is the React entry point. It imports React, React DOM, and the root component, and it calls createRoot() to mount the application.

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

The StrictMode component is a development-only wrapper that helps find common bugs. It double-invokes certain functions to detect side effects. It has no effect in production.

App.jsx is the root component. It is the top of the component tree. Every other component is a descendant of App. The scaffolded file contains a simple counter and a couple of logos.

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.jsx</code> and save to test HMR
        </p>
      </div>
    </>
  )
}

export default App

App.css is the stylesheet for the App component. Vite processes the CSS and injects it into the page. The styles are scoped to the component by convention, not by the tool. The developer chooses the naming convention.

index.css is the global stylesheet. It is imported in main.jsx. The styles apply to the entire application.

src/assets/ holds assets that Vite processes. The react.svg file is imported in App.jsx with import reactLogo from './assets/react.svg'. Vite transforms the import into a URL. The file name includes a content hash in the production build.


d. The node_modules/ Directory

The node_modules/ directory holds the installed packages. It is created by npm install. It is not committed to version control. It is recreated on every machine that runs npm install.

The directory can contain thousands of folders. Each folder is a package. Each package has its own package.json, its own dependencies, and its own code. The package manager resolves the dependency tree and flattens it where possible.

The node_modules/ directory should never be edited by hand. Changes are overwritten on the next npm install. The correct way to modify a dependency is to use the package manager or to use a patch tool.

The directory is listed in .gitignore. If it is committed, the repository becomes huge and the versions are no longer reproducible. The lockfile is the correct way to record the dependency tree, not the node_modules/ folder.


e. Adding Structure as the Project Grows

The scaffolded structure is minimal. A real application adds folders as it grows. The common conventions are:

src/
├── components/
│   ├── Button.jsx
│   ├── Button.css
│   └── Card.jsx
├── hooks/
│   ├── useAuth.js
│   └── useFetch.js
├── pages/
│   ├── Home.jsx
│   └── About.jsx
├── services/
│   ├── api.js
│   └── auth.js
├── utils/
│   ├── format.js
│   └── validation.js
├── App.jsx
├── App.css
├── index.css
└── main.jsx

components/ holds reusable UI components. Each component has its own file and, optionally, its own stylesheet. The folder grows with the design system.

hooks/ holds custom hooks. A custom hook is a function that starts with use and calls other hooks. It encapsulates stateful logic that multiple components share.

pages/ holds the components that correspond to routes. In a project that uses React Router or Next.js, each page is a route. The folder structure mirrors the URL structure.

services/ holds the code that talks to the outside world: API calls, authentication, local storage. The components import the services and call their methods.

utils/ holds pure functions: formatters, validators, parsers. They have no React dependencies and can be tested in isolation.

The structure is a convention, not a requirement. React does not enforce any folder layout. The convention is what the community has settled on. Following it makes the project familiar to other React developers.


Complete Example Session

This session explores the structure of a freshly scaffolded Vite React project.

# ============================================
# PART 1: THE ROOT DIRECTORY
# ============================================

ls -la

# Output:
# drwxr-xr-x  node_modules
# drwxr-xr-x  public
# drwxr-xr-x  src
# -rw-r--r--  .gitignore
# -rw-r--r--  eslint.config.js
# -rw-r--r--  index.html
# -rw-r--r--  package.json
# -rw-r--r--  package-lock.json
# -rw-r--r--  README.md
# -rw-r--r--  vite.config.js

# ============================================
# PART 2: THE index.html FILE
# ============================================

cat index.html

# Output:
# <!doctype html>
# <html lang="en">
#   <head>
#     <meta charset="UTF-8" />
#     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
#     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
#     <title>Vite + React</title>
#   </head>
#   <body>
#     <div id="root"></div>
#     <script type="module" src="/src/main.jsx"></script>
#   </body>
# </html>

# The <div id="root"> is where React mounts.
# The <script> tag loads the application.

# ============================================
# PART 3: THE src/ DIRECTORY
# ============================================

ls -la src/

# Output:
# drwxr-xr-x  assets
# -rw-r--r--  App.css
# -rw-r--r--  App.jsx
# -rw-r--r--  index.css
# -rw-r--r--  main.jsx

# ============================================
# PART 4: THE main.jsx FILE
# ============================================

cat src/main.jsx

# Output:
# import { StrictMode } from 'react'
# import { createRoot } from 'react-dom/client'
# import App from './App.jsx'
# import './index.css'
#
# createRoot(document.getElementById('root')).render(
#   <StrictMode>
#     <App />
#   </StrictMode>,
# )

# main.jsx mounts the App component into the #root div.

# ============================================
# PART 5: THE App.jsx FILE
# ============================================

cat src/App.jsx

# Output:
# import { useState } from 'react'
# import reactLogo from './assets/react.svg'
# import viteLogo from '/vite.svg'
# import './App.css'
#
# function App() {
#   const [count, setCount] = useState(0)
#
#   return (
#     <>
#       <div>
#         <a href="https://vite.dev" target="_blank">
#           <img src={viteLogo} className="logo" alt="Vite logo" />
#         </a>
#         <a href="https://react.dev" target="_blank">
#           <img src={reactLogo} className="logo react" alt="React logo" />
#         </a>
#       </div>
#       <h1>Vite + React</h1>
#       <div className="card">
#         <button onClick={() => setCount((count) => count + 1)}>
#           count is {count}
#         </button>
#         <p>
#           Edit <code>src/App.jsx</code> and save to test HMR
#         </p>
#       </div>
#     </>
#   )
# }
#
# export default App

# App is the root component.
# It imports the CSS and the assets.

# ============================================
# PART 6: THE public/ DIRECTORY
# ============================================

ls -la public/

# Output:
# -rw-r--r--  vite.svg

# The public folder holds static assets.
# The vite.svg file is the favicon.

# ============================================
# PART 7: THE src/assets/ DIRECTORY
# ============================================

ls -la src/assets/

# Output:
# -rw-r--r--  react.svg

# The src/assets folder holds processed assets.
# The react.svg file is imported in App.jsx.

# ============================================
# PART 8: THE package.json SCRIPTS
# ============================================

cat package.json | grep -A 5 scripts

# Output:
# "scripts": {
#   "dev": "vite",
#   "build": "vite build",
#   "lint": "eslint .",
#   "preview": "vite preview"
# }

# The scripts are the project commands.

# ============================================
# PART 9: THE vite.config.js FILE
# ============================================

cat vite.config.js

# Output:
# import { defineConfig } from 'vite'
# import react from '@vitejs/plugin-react'
#
# export default defineConfig({
#   plugins: [react()],
# })

# The config defines the React plugin.

# ============================================
# PART 10: THE DIRECTORY SUMMARY
# ============================================

# index.html     → entry point (root)
# src/main.jsx   → React entry point
# src/App.jsx    → root component
# src/App.css    → root component styles
# src/index.css  → global styles
# src/assets/    → processed assets
# public/        → unprocessed assets
# package.json   → dependencies and scripts
# vite.config.js → build configuration

The ten parts cover the root directory, the index.html file, the src/ directory, the main.jsx file, the App.jsx file, the public/ directory, the src/assets/ directory, the package.json scripts, the vite.config.js file, and the directory summary.


Quick Reference

The Root Files

FilePurpose
index.htmlEntry point
package.jsonDependencies and scripts
package-lock.jsonExact dependency versions
vite.config.jsBuild configuration
eslint.config.jsLinting configuration
.gitignoreFiles Git should ignore
README.mdProject documentation

The src/ Files

FilePurpose
main.jsxReact entry point
App.jsxRoot component
App.cssRoot component styles
index.cssGlobal styles
assets/Processed assets

The Asset Directories

DirectoryProcessingExample
public/Not processedfavicon.ico, robots.txt
src/assets/Processed by ViteImages, SVGs

The Common Folders

FolderPurpose
components/Reusable UI components
hooks/Custom hooks
pages/Route components
services/API and external calls
utils/Pure functions

Best Practices

✅ Do This:

<!-- Keep index.html at the project root -->
<!-- The script tag points to src/main.jsx -->                    // ✅
// Import assets from src/assets
import logo from './assets/logo.svg'                             // ✅
# Commit package.json and package-lock.json
git add package.json package-lock.json                           # ✅
# Ignore node_modules in Git
echo "node_modules/" >> .gitignore                               # ✅

❌ Don’t Do This:

# Don't move index.html into public/
# Vite treats it as the entry point                               // ❌
# Don't commit node_modules
git add node_modules/                                            // ❌
// Don't edit node_modules by hand
// Changes are overwritten on the next install                    // ❌
# Don't delete the lockfile
rm package-lock.json                                             // ❌

Common Pitfalls

PitfallWhy It HappensFix
index.html not foundMoved to public/Keep at root
Asset not loadingWrong folderUse public/ or src/assets/
Dependencies missingnode_modules/ deletedRun npm install
Different versionsLockfile not committedCommit the lockfile
Slow buildNo optimizationCheck vite.config.js

Real-World Examples

1. The Root Directory

ls -la

2. The index.html

cat index.html

3. The src/ Directory

ls -la src/

4. The main.jsx

cat src/main.jsx

5. The App.jsx

cat src/App.jsx

6. The public/ Directory

ls -la public/

7. The src/assets/ Directory

ls -la src/assets/

8. The package.json Scripts

cat package.json | grep -A 5 scripts

9. The vite.config.js

cat vite.config.js

10. The Common Folders

mkdir -p src/components src/hooks src/pages src/services src/utils

Visual

The Project Structure

┌──────────────────────────────────────────────┐
│  my-react-app/                               │
│  ├── node_modules/    ← installed packages   │
│  ├── public/          ← unprocessed assets   │
│  │   └── vite.svg                            │
│  ├── src/             ← application code     │
│  │   ├── assets/      ← processed assets     │
│  │   │   └── react.svg                       │
│  │   ├── App.css      ← component styles     │
│  │   ├── App.jsx      ← root component       │
│  │   ├── index.css    ← global styles        │
│  │   └── main.jsx     ← React entry point    │
│  ├── .gitignore                              │
│  ├── eslint.config.js                        │
│  ├── index.html       ← HTML entry point     │
│  ├── package.json     ← dependencies         │
│  ├── package-lock.json← exact versions       │
│  ├── README.md                               │
│  └── vite.config.js   ← build config         │
│                                              │
└──────────────────────────────────────────────┘

The Module Graph

┌──────────────────────────────────────────────┐
│  index.html                                  │
│    └── <script src="/src/main.jsx">          │
│         └── import App from './App.jsx'      │
│              ├── import reactLogo from ...   │
│              ├── import viteLogo from ...    │
│              ├── import './App.css'          │
│              └── import { useState } ...     │
│                                              │
│  Vite follows the imports.                   │
│  The graph is the application.               │
│                                              │
└──────────────────────────────────────────────┘

The Asset Processing

┌──────────────────────────────────────────────┐
│  public/                                     │
│    └─ copied as-is to dist/                  │
│    └─ referenced with /path                  │
│                                              │
│  src/assets/                                 │
│    └─ processed by Vite                      │
│    └─ hashed in production                   │
│    └─ imported in JavaScript                 │
│                                              │
└──────────────────────────────────────────────┘

The Common Folder Structure

┌──────────────────────────────────────────────┐
│  src/                                        │
│  ├── components/  ← reusable UI              │
│  ├── hooks/       ← custom hooks             │
│  ├── pages/       ← route components         │
│  ├── services/    ← API calls                │
│  ├── utils/       ← pure functions           │
│  ├── App.jsx                                 │
│  ├── App.css                                 │
│  ├── index.css                               │
│  └── main.jsx                                │
│                                              │
│  The structure grows with the application.   │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Entry pointindex.html at the root
React entrysrc/main.jsx
Root componentsrc/App.jsx
Global stylessrc/index.css
Component stylessrc/App.css
Processed assetssrc/assets/
Unprocessed assetspublic/
Dependenciespackage.json
Lockfilepackage-lock.json
Configurationvite.config.js
Installed packagesnode_modules/

Key takeaways:

  • index.html is the entry point and it lives at the root of the project. Vite treats it as part of the module graph. The <script type="module"> tag inside it points to src/main.jsx. This is different from Create React App, where index.html lived in public/ .
  • src/main.jsx mounts the React application. It imports React, React DOM, and the root component, and it calls createRoot() to render into the <div id="root"> element. The StrictMode wrapper helps find bugs in development .
  • src/App.jsx is the root component. It is the top of the component tree. Every other component is a descendant of App. It imports the CSS and the assets it needs .
  • public/ holds unprocessed assets. Files in public/ are copied to the build output as-is. They are referenced with an absolute path. A favicon, a robots.txt, and a manifest.json go here .
  • src/assets/ holds processed assets. Files in src/assets/ are imported in JavaScript. Vite processes them, optimizes them, and includes a content hash in the file name for the production build .
  • package.json is the project manifest. It lists the dependencies, the scripts, and the metadata. The scripts object defines the commands: dev, build, preview, and lint .
  • node_modules/ holds the installed packages. It is created by npm install. It is not committed to version control. The lockfile records the exact versions, and the node_modules/ folder is recreated from it .
  • The structure grows with the application. The scaffolded project is minimal. Real projects add folders for components, hooks, pages, services, and utilities. The conventions are community-driven, not enforced by React.

Remember: A React project has a defined structure. index.html is the entry point. main.jsx mounts the application. App.jsx is the root component. public/ holds unprocessed assets. src/assets/ holds processed assets. package.json lists the dependencies and the scripts. vite.config.js configures the build. node_modules/ holds the installed packages. The structure is a convention, and following it makes the project navigable.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!