Setting up a React project
A React app needs a small amount of tooling around it before any of the code runs. Something has to turn JSX into JavaScript a browser can execute, and something has to serve the result while you work on it. Vite does both, and it scaffolds a working project in about thirty seconds. This chapter goes from an empty folder to a running app, then traces the one path that matters most: how a component ends up on the screen.
Creating a project
Vite is a build tool with a project generator built in. Run it with npm create, answer two prompts, and you have a project:
npm create vite@latest my-react-app
cd my-react-app
npm install
npm run devThe first command asks which framework you want (choose React) and which variant (choose JavaScript, or TypeScript if you're using it). It writes a folder called my-react-app with everything a React project needs. npm install downloads the dependencies listed in package.json, which is where React itself comes from. npm run dev starts the development server and prints a local address, usually http://localhost:5173. Open that in a browser and the starter app is running.
The generated folder has a handful of files that matter early on:
index.html: the single HTML page the browser loads.src/main.jsx: the entry point, where React attaches to that page.src/App.jsx: your top-level component, and the file you'll edit first.src/assets/: images and other static files you import into components.package.json: dependencies and thedev,build, andpreviewscripts.vite.config.js: build configuration, which you can leave alone for a long time.
How the pieces connect
Three files hand off to each other in a straight line. Start at index.html, which is deliberately almost empty:
<body>
<div id="root"></div>
<!-- followed by a <script type="module" src="/src/main.jsx"> tag -->
</body>That empty <div id="root"> is the spot on the page React is allowed to fill. Everything your app renders lands inside it. The module <script> tag below it loads main.jsx, which is where React takes over:
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>
)Read that call right to left. document.getElementById('root') finds the empty div. createRoot wraps it in a React root, meaning React now owns the contents of that element and will keep them in sync with your components. .render(...) tells it what to put there: the App component.
<StrictMode> is a development-only wrapper that runs extra checks on everything inside it and warns about patterns likely to cause bugs. It adds nothing to the page and switches itself off in a production build. Its most visible effect turns up in Effects, where it deliberately mounts each component twice during development.
App.jsx is an ordinary component that exports itself:
export default function App() {
return <h1>Hello from React</h1>
}That is the whole chain. The browser loads index.html, the script loads main.jsx, main.jsx calls createRoot on the root div and renders <App />, and App returns the JSX that becomes the heading you see. Every component you write from here on sits somewhere inside App, so it reaches the page through this same path. main.jsx is usually written once and rarely touched again; almost all of your work happens in App.jsx and the components it pulls in.
Version note
React 17 and earlier used ReactDOM.render(<App />, document.getElementById('root')) as the entry point. React 18 introduced createRoot, which is what turns on concurrent rendering, and left the old call working with a deprecation warning. React 19 removed it, so ReactDOM.render now throws an error. On a tutorial that still uses it, createRoot is the line to write in its place. See History and versions for what changed.
The dev server
npm run dev starts Vite's development server, and it stays running in the terminal while you work. Its job is to serve your app and to react to your edits.
Save a change to App.jsx and the browser updates almost immediately, without a manual refresh. That's hot module replacement: Vite pushes the changed module to the page and swaps it in place, leaving the rest of the app running. A counter you'd clicked up to seven usually still reads seven after the edit, so you keep whatever state you had set up while you tweak the markup around it. Some changes still force a full reload, and Vite decides that for you.
Two other scripts come with the project. npm run build produces an optimized bundle in a dist folder, which is what you deploy. npm run preview serves that built output locally so you can check it before shipping. During development, npm run dev is the only one you need.
Importing static assets
Images live under src/assets and come into a component through an import:
import logo from './assets/logo.png'
export default function Header() {
return <img src={logo} alt="Company logo" />
}The import gives you a variable holding the final URL of that image, which you then pass to src in curly braces. Writing src="./assets/logo.png" as a plain string tends to break, because the paths in your source folder are rarely the paths in the built output.
Importing the file instead puts the build tool in the loop. It sees that your code depends on logo.png, copies it into the output, and gives you back whatever URL that file ends up at. Move your component into a different folder and the relative import still resolves. Misspell the filename and you find out immediately, because the import fails, rather than discovering a broken image in production. The same pattern works for SVGs, fonts, and any other file the build tool knows how to handle.
index.html has an empty div with the id root, main.jsx grabs that div with createRoot and renders <App /> into it, and App.jsx is the component that says what appears. Once you can follow that path, you know exactly where your own code plugs in. Next up: Components, the building blocks you'll fill that App.jsx with.

