Project Structure
A fresh React project created with Vite is intentionally lean, but every file has a job. Understanding how index.html, main.jsx, and App.jsx chain together to put your UI on screen demystifies the framework and saves you hours of guessing later. Once you know the wiring, you can confidently add components, organize folders, and grow the app without it turning into a mess.
A tour of the files
After running npm create vite@latest my-app -- --template react and installing dependencies, you get a tree like this:
my-app/
├── index.html # the single HTML entry point
├── package.json # dependencies and npm scripts
├── vite.config.js # Vite + React plugin configuration
├── public/ # static files copied as-is to the build
│ └── vite.svg
└── src/
├── main.jsx # bootstraps React onto the DOM
├── App.jsx # your root component
├── App.css
├── index.css # global styles
└── assets/
└── react.svg
The two things worth internalizing right away: index.html lives at the root (not in public/), and almost everything you write goes inside src/.
The HTML entry point
Unlike older tooling, Vite treats index.html as the real entry point of your app. It is a normal HTML file with one important <div> and a <script> tag:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
The empty <div id="root"> is the mount point—React will fill it with your entire component tree. The <script type="module"> line tells the browser to load main.jsx, which kicks everything off.
The render entry point: main.jsx
main.jsx is the bridge between the static HTML and your React components. It grabs the #root element and tells React to render into it using the React 18+ createRoot API:
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>
);
createRoot creates a root tied to the DOM node, and .render() mounts your <App /> into it. StrictMode is a development-only wrapper that surfaces common bugs early by intentionally double-invoking certain functions—it renders nothing visible and disappears in production builds.
Don’t add application logic to
main.jsx. Keep it to the three lines above—create the root, renderApp, and import global CSS. Everything else belongs in components.
The root component: App.jsx
App.jsx is the top of your component tree. Every other component is rendered, directly or indirectly, from here:
import { useState } from "react";
import Counter from "./components/Counter.jsx";
import "./App.css";
function App() {
const [name] = useState("Devcraftly");
return (
<main>
<h1>Welcome to {name}</h1>
<Counter />
</main>
);
}
export default App;
Organizing components
For anything beyond a demo, create a components/ folder under src/ and give each component its own file. A small, focused component looks like this:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((c) => c + 1)}>
Clicked {count} times
</button>
);
}
export default Counter;
A sensible convention as the app grows:
src/
├── components/ # reusable UI pieces (Button, Card, Counter)
├── pages/ # top-level views, one per route
├── hooks/ # custom hooks (useAuth, useFetch)
├── lib/ # plain helpers and API clients
├── assets/ # images/fonts imported by components
└── App.jsx
public/ versus src/assets/
Both folders hold static files, but they behave differently—choosing the right one avoids broken images.
| Location | How to reference | Processed by Vite? | Use for |
|---|---|---|---|
src/assets/ | import logo from "./assets/logo.png" | Yes (hashed, bundled) | Images used inside components |
public/ | Absolute path /favicon.ico | No (copied verbatim) | favicon, robots.txt, files needing stable URLs |
import logo from "./assets/logo.png";
function Header() {
return <img src={logo} alt="Company logo" width={120} />;
}
export default Header;
package.json scripts
package.json lists your dependencies and the commands you’ll run daily:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
Running npm run dev starts the hot-reloading dev server:
Output:
VITE v6.0.0 ready in 312 ms
➜ Local: http://localhost:5173/
➜ press h + enter to show help
Best Practices
- Keep
main.jsxminimal—only create the root, renderApp, and import global CSS. - Give each component its own file and name the file after the component (
Counter.jsx). - Group code by purpose:
components/,pages/,hooks/, andlib/. importimages fromsrc/assets/so Vite optimizes and fingerprints them; reservepublic/for files that need fixed URLs.- Co-locate a component’s CSS, tests, and helpers next to it once a feature grows.
- Use the
@/path alias (configured invite.config.js) to avoid brittle../../import chains.