Skip to content
React rc getting-started 4 min read

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, render App, 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.

LocationHow to referenceProcessed by Vite?Use for
src/assets/import logo from "./assets/logo.png"Yes (hashed, bundled)Images used inside components
public/Absolute path /favicon.icoNo (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.jsx minimal—only create the root, render App, 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/, and lib/.
  • import images from src/assets/ so Vite optimizes and fingerprints them; reserve public/ 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 in vite.config.js) to avoid brittle ../../ import chains.
Last updated June 14, 2026
Was this helpful?