The template creates about a dozen files. You will work in two of them most of the time, but knowing what the others do means that nothing in your project is a mystery box. This tour uses the files that npm create vite@latest my-app -- --template react generated, checked on create-vite 9.3.1. Newer versions may differ in small details, but the shape stays the same.
The folder at a glance
my-app/
├── index.html
├── package.json
├── vite.config.js
├── .gitignore
├── .oxlintrc.json
├── README.md
├── public/
│ ├── favicon.svg
│ └── icons.svg
└── src/
├── main.jsx
├── App.jsx
├── App.css
├── index.css
└── assets/
├── hero.png
├── react.svg
└── vite.svgAfter npm install you also get node_modules/ (the downloaded packages) and a lock file, and after npm run build you get dist/ (the finished site). The last two are generated: never edit them by hand.
index.html: the only page
A React app of this kind is a single-page application: the browser loads one HTML file, and React draws everything else with JavaScript. Here it is, complete:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<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>Two lines matter. <div id="root"></div> is an empty container where React will put the interface. <script type="module" src="/src/main.jsx"> loads your code as a JavaScript module. This is where you change the page title (<title>) and add things that must exist before React starts, such as fonts or analytics tags. Because the visible content is made by JavaScript, search engines and link previews see an empty page unless you use server rendering, a reason some teams pick a framework like Next.js for content sites.
main.jsx: the entry point
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)Read it from the bottom up. document.getElementById('root') finds the container from index.html. createRoot(...) tells React DOM (the part of React that talks to the browser) to take control of that element. .render(...) draws the given element tree inside it, here <App /> wrapped in <StrictMode>. The imports at the top bring in React's tools, the global stylesheet and your App component.
StrictMode does not show anything. In development it runs extra checks: it calls components twice and runs effects an extra time to reveal bugs such as impure rendering. Its checks disappear in the production build. The StrictMode and debugging lesson explains what to expect. The lesson editors in this course mount your App component the same way as this file does.
You rarely edit main.jsx. Later you will wrap <App /> in things like a router or a context provider here.
App.jsx: the root component
App is the top of your component tree, and where you replace the starter content with your own. The template's version is long because it contains the demo page; the essential structure is:
import { useState } from 'react'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
<h1>Get started</h1>
<button type="button" onClick={() => setCount((count) => count + 1)}>
Count is {count}
</button>
</>
)
}
export default App(This is a trimmed copy of the template's component.) Notice import './App.css': Vite lets you import a CSS file straight into a component, and adds its rules to the page. The <>...</> is a fragment, which groups elements without adding an extra element to the page; the next lesson explains it.
The src/assets folder and the public folder
There are two places for images and other files, and the difference matters.
src/assets |
public |
|
|---|---|---|
| How you use a file | import logo from './assets/react.svg' then <img src={logo} /> |
write the path directly: <img src="/favicon.svg" /> |
| Processed by Vite | yes: the file name gets a content hash, small files may be inlined | no: copied exactly as it is |
| Missing file | build error, so you find out early | a broken link you may not notice |
| Good for | images and fonts your components use | robots.txt, favicons, files that need a fixed URL |
Putting an image in src/assets is the default choice. In the production build above, react.svg became dist/assets/react-CHdo91hT.svg: the hash lets browsers cache it forever and still refetch when the file changes.
CSS files
src/index.css holds global styles such as fonts and colours, imported once in main.jsx. src/App.css holds styles for the demo page, imported by App.jsx. They are ordinary CSS. If you are new to CSS, the CSS course covers it, and the lesson on styling React apps shows the options. You can delete the template's CSS and write your own.
package.json
{
"name": "my-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "oxlint",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.3.1",
"react-dom": "^19.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^6.1.2",
"oxlint": "^1.88.0",
"vite": "^8.3.4"
}
}scriptsare the commands you run withnpm run.dependenciesare the packages your app needs when it runs:react(the core) andreact-dom(the browser renderer). They are two packages because React can render to other places too, such as phones.devDependenciesare tools used only while building: Vite, the React plugin that compiles JSX, and the linter. (The template also lists the@typespackages, used by editors for hints.)"type": "module"makes.jsfiles useimport/export.- The
^in^19.3.1means "this version or a newer compatible one".npm installrecords the exact versions in the lock file (package-lock.json), which you should commit so everyone gets the same versions.
Add a package with npm install name, and remove one with npm uninstall name; npm edits package.json for you.
vite.config.js and the other files
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [react()],
})The configuration is tiny: it enables the React plugin, which compiles JSX and adds hot reloading that keeps component state while you edit. You will touch this file when you need something like a proxy for API calls or a different base path for deployment.
.gitignore lists what Git should not track (node_modules, dist, logs, editor folders). .oxlintrc.json configures the linter, with two React rules switched on: one enforces the rules of hooks (an error) and one warns when a file exports things that break fast refresh. README.md is documentation for the repository, so replace it with a description of your project.
How a page gets built: the whole path
- The browser requests
/and getsindex.htmlwith an empty#root. - The
<script type="module">loadssrc/main.jsx. main.jsximports React,index.cssand theAppcomponent, and each of those imports its own files.createRoot(...).render(<App />)runs, React callsApp(), which returns JSX, and React puts the matching elements into#root.- When state changes (a button click), React calls the affected component functions again and updates only the parts of the page that changed.
Everything you learn in this course happens in step 4 and 5: components that return JSX, and state that makes them run again.
Suggested structure as the app grows
The template keeps everything in src. As you add components, a common layout is:
src/
├── components/ reusable pieces: Button.jsx, TaskItem.jsx
├── pages/ one component per screen: Home.jsx, About.jsx
├── hooks/ custom hooks: useLocalStorage.js
├── App.jsx
└── main.jsxThere is no required structure. Start flat, and create a folder when you have several files that belong together. Name component files after the component, with a capital letter (TaskItem.jsx), because the name of a component must start with a capital.
Common mistakes
- Editing the files in
distornode_modules, which are regenerated. - Putting images that components use in
public, then breaking the path when the site is deployed under a sub-folder. - Deleting the
<div id="root">fromindex.html, which makes React fail with "Target container is not a DOM element". - Renaming
main.jsxwithout changing the script tag inindex.html. - Naming a component file in lower case, or a component function in lower case; React treats lower-case tags as HTML elements.
- Forgetting the
.jsxextension for files that contain JSX; Vite expects it.
Exercise
- Open your project and find each file listed in this lesson. Delete everything inside
App.jsx'sreturnand replace it with anh1. - Change the
<title>inindex.htmland check the browser tab. - Put an image in
src/assets, import it intoApp.jsxand show it with<img>. Then look indistafternpm run buildto see its new name. - Add a
robots.txtfile topublicand open/robots.txton the dev server.
Key points
- index.html has an empty div with id root and a script tag that loads src/main.jsx; everything else is built by React.
- main.jsx calls createRoot on that div and renders the App component inside StrictMode.
- Files in src are processed by Vite and can be imported; files in public are copied as they are and referred to by absolute path.
- package.json lists the dependencies and the scripts; node_modules and dist are generated and not committed to Git.