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
| File | Purpose |
|---|---|
index.html | Entry point |
package.json | Dependencies and scripts |
package-lock.json | Exact dependency versions |
vite.config.js | Build configuration |
eslint.config.js | Linting configuration |
.gitignore | Files Git should ignore |
README.md | Project documentation |
The src/ Files
| File | Purpose |
|---|---|
main.jsx | React entry point |
App.jsx | Root component |
App.css | Root component styles |
index.css | Global styles |
assets/ | Processed assets |
The Asset Directories
| Directory | Processing | Example |
|---|---|---|
public/ | Not processed | favicon.ico, robots.txt |
src/assets/ | Processed by Vite | Images, SVGs |
The Common Folders
| Folder | Purpose |
|---|---|
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
| Pitfall | Why It Happens | Fix |
|---|---|---|
index.html not found | Moved to public/ | Keep at root |
| Asset not loading | Wrong folder | Use public/ or src/assets/ |
| Dependencies missing | node_modules/ deleted | Run npm install |
| Different versions | Lockfile not committed | Commit the lockfile |
| Slow build | No optimization | Check 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
| Item | Value |
|---|---|
| Entry point | index.html at the root |
| React entry | src/main.jsx |
| Root component | src/App.jsx |
| Global styles | src/index.css |
| Component styles | src/App.css |
| Processed assets | src/assets/ |
| Unprocessed assets | public/ |
| Dependencies | package.json |
| Lockfile | package-lock.json |
| Configuration | vite.config.js |
| Installed packages | node_modules/ |
Key takeaways:
index.htmlis 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 tosrc/main.jsx. This is different from Create React App, whereindex.htmllived inpublic/.src/main.jsxmounts the React application. It imports React, React DOM, and the root component, and it callscreateRoot()to render into the<div id="root">element. TheStrictModewrapper helps find bugs in development .src/App.jsxis the root component. It is the top of the component tree. Every other component is a descendant ofApp. It imports the CSS and the assets it needs .public/holds unprocessed assets. Files inpublic/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 insrc/assets/are imported in JavaScript. Vite processes them, optimizes them, and includes a content hash in the file name for the production build .package.jsonis the project manifest. It lists the dependencies, the scripts, and the metadata. Thescriptsobject defines the commands:dev,build,preview, andlint.node_modules/holds the installed packages. It is created bynpm install. It is not committed to version control. The lockfile records the exact versions, and thenode_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!