Creating a React App with Vite
Scaffolding, the dev server, and what the build actually produces.
What you will be able to do
- Explain what Vite is, and how it differs from React
- Scaffold a React + TypeScript project and recognise every file it creates
- Read package.json and say what each script actually runs
- Run the dev server, and describe what Hot Module Replacement does
- Produce a production build and explain what is inside dist/
- Explain why built filenames carry hashes
- Tell development from production - and why type errors only stop one of them
The idea, in plain English
React is the library you write your UI with. It does not start a server, compile your TypeScript, or bundle your files for the web. Vite does those jobs. It is a development server and a build tool, and it is the standard way to start a React project today.
Vite has two modes, and they matter more than any file in the project. npm run dev starts a local server at localhost:5173 that compiles files on demand and updates the browser the moment you save. npm run build turns your source into a folder called dist - plain HTML, JavaScript, and CSS that any web server or CDN can host.
Everything else in this lesson sits in one of those two. The dev server is for you, while you work: fast, forgiving, and never deployed. The dist folder is for your users: optimised, hashed, and the only thing that goes to production.
Every number and filename in this lesson comes from a real project created with create-vite 9 and Vite 8. The exact versions, and a few filenames, will differ by the time you run it - the shape will not.
Worked example: From npm create to a deployed dist folder.
React is the UI; Vite is the tooling
React gives you components, JSX, props, state, hooks, and the rendering model. Vite gives you a dev server, a build, module handling, and production output. Neither replaces the other, and a React project needs both.
That split explains the files. src/ is yours and is mostly React. index.html, vite.config.ts, and the npm scripts are Vite. The tsconfig files are TypeScript. The @vitejs/plugin-react entry in vite.config.ts is the bridge that teaches Vite to handle JSX and React Fast Refresh.
ReactComponents, JSX, state, props, hooks - the UI itself.ViteThe dev server, module handling, and the production build.@vitejs/plugin-reactTeaches Vite about JSX and keeps state across edits.TypeScript (tsc)Type checking - which Vite itself deliberately does not do.npmInstalls the packages and runs the scripts in package.json.Scaffolding a project
npm create vite@latest runs the create-vite tool and asks for a project name, a framework, and a variant. Choose React and TypeScript for this course.
You can skip the questions: npm create vite@latest my-react-app -- --template react-ts. The lone -- matters. Everything after it is passed through npm to create-vite, so --template reaches the tool instead of being read by npm.
The scaffold contains no node_modules yet. npm install reads package.json, downloads every listed package into node_modules/, and records the exact versions in package-lock.json. It installs into this project only - nothing is installed globally.
Watch out: Vite 8 needs Node.js 20.19 or newer (or 22.12+). An older Node fails at npm run dev with an engine or syntax error that looks unrelated. Check with node --version first.
package.json and its scripts
package.json names the project, lists its dependencies, and defines its scripts. React and react-dom are the only runtime dependencies; everything else - Vite, TypeScript, the React plugin, type definitions, and a linter - is a devDependency, needed to build the app rather than to run it.
npm run dev runs "vite". npm run preview runs "vite preview". npm run build runs "tsc -b && vite build" - TypeScript first, then Vite, and the && means the Vite build never starts if the type check fails.
That ordering exists because Vite strips TypeScript types without checking them. It is why the dev server feels instant - and why the dev server will happily run code with a type error that npm run build then refuses. Your editor shows type errors as you type; the build is the gate.
npm run devvite - the dev server on localhost:5173, with instant updates.npm run buildtsc -b && vite build - type-check, then build into dist/.npm run previewvite preview - serves the built dist/ on localhost:4173.npm run lintoxlint - flags suspicious code. Older templates used ESLint here.Tip: The type check lives in the build script, not in the dev server. Keep your editor TypeScript integration on, or you will find type errors only at build time.
The dev server and Hot Module Replacement
npm run dev prints a local address - http://localhost:5173 - and serves your app from src/ directly. When the browser asks for /src/main.tsx, Vite compiles that one file to JavaScript and sends it back; nothing is bundled ahead of time, which is why it starts in well under a second.
Save a file and Hot Module Replacement swaps just that module into the running page. With the React plugin it goes further: change the heading of a component that holds state, and the heading updates while the state stays exactly where it was. Click a counter to 3, edit the markup, and it still says 3 - no reload.
The dev server is not meant for production. It serves unbundled source, keeps development-only checks switched on, and is built for one person working locally.
The build, and what dist/ contains
npm run build reads index.html, follows every import from main.tsx, and writes the result to dist/. On the default template it transforms 20 modules into one JavaScript file, one CSS file, and the images they reference, in well under a second.
dist/index.html is rewritten. The script tag that pointed at /src/main.tsx now points at /assets/index-[hash].js and moves into the head, and a stylesheet link is added. Your TypeScript and JSX are nowhere in dist - only the browser-ready output.
Files in public/ are copied into dist unchanged and keep their names. Files you import from src/ - images, CSS - are processed and get hashed names in dist/assets. Use public/ for things that need a fixed URL, such as a favicon.
index.html0.45 kB. The shell, now pointing at the hashed bundle.assets/index-[hash].js222 kB, 69 kB gzipped. Your code plus React itself - most of it is React.assets/index-[hash].css4 kB. Every imported stylesheet, combined.assets/hero-[hash].pngImported images, hashed.favicon.svgFrom public/ - copied as-is, name unchanged.Why the filenames carry hashes
The hash in index-BRDr3nmD.js is a fingerprint of the file contents. Change one line of App.tsx and rebuild, and the JavaScript file gets a new name - while the CSS and images keep theirs, because they did not change.
That lets a server tell browsers and CDNs to cache every asset for a year. A returning visitor downloads only the files whose names changed and reuses the rest. Only index.html must never be cached for long, because it is what points at the current names.
Tip: If users report seeing an old version after a deploy, the usual culprit is a cached index.html still pointing at old hashed files - not the hashed files themselves.
Development against production
Development: src/ goes through the Vite dev server to your browser, with instant updates, full error messages, and StrictMode double-rendering on. Production: src/ goes through vite build into dist/, which is uploaded to a web server or CDN and served to users.
npm run preview is the bridge. It serves the built dist/ on localhost:4173, so you can check the real production output before deploying it. It is a check, not a production server.
Vite does not run your backend. A typical setup has the React app on localhost:5173 in development and an Express API on localhost:3000, as two separate processes - and in production, dist/ on a CDN with the API deployed separately.
Syntax and examples
# Scaffold a React + TypeScript project. The -- passes --template to create-vite.
npm create vite@latest my-react-app -- --template react-ts
cd my-react-app
npm install # reads package.json, fills node_modules/
npm run dev # http://localhost:5173my-react-app/
├── public/
│ ├── favicon.svg copied to dist/ unchanged
│ └── icons.svg
├── src/
│ ├── assets/ images you import - hashed at build time
│ ├── App.css
│ ├── App.tsx the top component
│ ├── index.css
│ └── main.tsx the entry point
├── .gitignore ignores node_modules and dist
├── .oxlintrc.json linter settings
├── index.html the page shell
├── package.json
├── README.md
├── tsconfig.json points at the two files below
├── tsconfig.app.json settings for your src/ code
├── tsconfig.node.json settings for vite.config.ts
└── vite.config.ts{
"name": "my-react-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@types/node": "^24.13.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.7",
"@vitejs/plugin-react": "^6.1.1",
"oxlint": "^1.81.0",
"typescript": "~6.0.2",
"vite": "^8.3.0"
}
}import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)$ npm run dev
VITE v8.3.1 ready in 86 ms
➜ Local: http://localhost:5173/// src/components/Header.tsx
function Header() {
return (
<header>
<h1>React Learning Platform</h1>
</header>
);
}
export default Header;
// src/App.tsx
import Header from './components/Header';
function App() {
return (
<div>
<Header />
<main>
<h2>Welcome</h2>
<p>Start learning React.</p>
</main>
</div>
);
}
export default App;$ npm run build
✓ 20 modules transformed.
dist/index.html 0.45 kB │ gzip: 0.29 kB
dist/assets/react-CHdo91hT.svg 4.12 kB │ gzip: 2.06 kB
dist/assets/vite-BF8QNONU.svg 8.70 kB │ gzip: 1.60 kB
dist/assets/hero-CLDdwZDr.png 13.05 kB
dist/assets/index-D64VDMd1.css 4.10 kB │ gzip: 1.47 kB
dist/assets/index-BRDr3nmD.js 222.52 kB │ gzip: 69.27 kB
✓ built in 320ms<!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-react-app</title>
<!-- /src/main.tsx is gone: the hashed bundle replaced it -->
<script type="module" crossorigin src="/assets/index-BRDr3nmD.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D64VDMd1.css">
</head>
<body>
<div id="root"></div>
</body>
</html>Change one line of App.tsx, run npm run build again:
assets/index-BRDr3nmD.js -> assets/index-C4uzZ7SY.js new name
assets/index-D64VDMd1.css unchanged
assets/hero-CLDdwZDr.png unchanged
Returning visitors download only the renamed file.// App.tsx
const lessons: number = 'one hundred ninety-two';
$ npm run dev page loads - Vite strips the types without checking them
$ npm run build
src/App.tsx(9,9): error TS2322: Type 'string' is not assignable to type 'number'.
build stops - dist/ is not producednpm run build # src/ -> dist/
npm run preview # serves dist/ on http://localhost:4173
# Deploy the contents of dist/ - not src/, not the project folder.Tip: Two ports to remember: 5173 is the dev server, 4173 is npm run preview. If something works on one and not the other, you are looking at a development-versus-production difference.
Watch out: The generated .gitignore ignores dist as well as node_modules. Both are rebuilt from your source - the build normally runs on the deployment server or in CI, not on your laptop.
Commands and folders
Everything you touch in a Vite project, in the order you meet it.
npm create vite@latestScaffolds a new project from a template.
npm create vite@latest app -- --template react-ts
npm installInstalls the project dependencies into node_modules/.
npm i
npm run devStarts the dev server with Hot Module Replacement.
http://localhost:5173
npm run buildType-checks, then builds the production output.
tsc -b && vite build
npm run previewServes dist/ locally to check the build.
http://localhost:4173
src/Your source code. What you edit.
public/Static files copied to dist/ with their names unchanged.
dist/The generated production build. What you deploy.
node_modules/Installed packages. Never edited, never committed.
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Run npm run dev, click the template counter to 3, then change the heading text in App.tsx and save. Watch the heading change while the count stays at 3.”
“Run npm run build and note the JavaScript filename in dist/assets. Change one line of App.tsx, build again, and see which filenames changed - and which did not.”
“Add const lessons: number = "192" to App.tsx. The dev server keeps working; npm run build fails. Fix it and build again.”
“Open dist/index.html and find where /src/main.tsx went. Then open the JavaScript file and search for a string you wrote in App.tsx.”
What usually goes wrong
React is the UI library; Vite is the tool that serves and builds it. Swapping Vite for another build tool would not change a line of your components.
dist/ is regenerated from scratch on every build, so any change made there is lost. Change src/ and build again.
✗ # fixing a typo directly in dist/assets/index-BRDr3nmD.js✓ # fix it in src/App.tsx, then:
npm run buildBoth are generated - node_modules from package-lock.json, dist from src/. The generated .gitignore already excludes both; keep it that way.
Browsers cannot run TypeScript or JSX, and the dev server is not built for real traffic. Production serves the built dist/ folder.
✗ npm run dev # on the production server✓ npm run build
# upload dist/ to the host or CDNVite strips types without checking them, so code with a type error runs fine in development. The tsc -b step in npm run build is what catches it.
Without it, npm consumes --template itself and create-vite never sees it, so you get the interactive questions or the wrong template.
✗ npm create vite@latest app --template react-ts✓ npm create vite@latest app -- --template react-tsBest practices
- Check your Node version before scaffolding - Vite 8 needs 20.19+ or 22.12+.
- Keep your editor TypeScript integration on; the dev server will not report type errors.
- Put fixed-URL files in public/ and import everything else from src/.
- Always run npm run preview before deploying, to see the real production output.
- Deploy dist/ only, and cache hashed assets for a long time and index.html briefly.
- Never edit or commit dist/ or node_modules/.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Scaffold a React + TypeScript project called react-course-app without answering any prompts, then start it.
Show hintHide hint
Pass the template on the command line - and remember the -- before it.
Show solutionHide solution
npm create vite@latest react-course-app -- --template react-ts
cd react-course-app
npm install
npm run devReplace App with a heading "My React Course" and a CourseCard component in src/components/CourseCard.tsx.
Show hintHide hint
One file per component, exported as default, imported into App.
Show solutionHide solution
// src/components/CourseCard.tsx
function CourseCard() {
return (
<article>
<h2>React Development</h2>
<p>Learn React from fundamentals to production.</p>
</article>
);
}
export default CourseCard;
// src/App.tsx
import CourseCard from './components/CourseCard';
function App() {
return (
<div>
<h1>My React Course</h1>
<CourseCard />
</div>
);
}
export default App;Explain what each of the three scripts - dev, build, preview - is for, and which one produces something you deploy.
Show hintHide hint
Two of them start a server; only one of them writes files.
Show solutionHide solution
dev Starts the Vite dev server on localhost:5173 for working
locally, with instant updates. Nothing is written to disk.
build Type-checks with tsc, then builds src/ into dist/ -
the only one that produces files, and dist/ is what you deploy.
preview Serves the already-built dist/ on localhost:4173, so you can
check the production output before deploying it.From npm create to a deployable dist/
Take one project through the whole lifecycle, and prove to yourself what each step produced.
- Scaffold react-course-app with the react-ts template and run it on the dev server.
- Add a CourseCard component and render it from App, and watch the change arrive without a reload.
- Run npm run build and list what dist/ contains, including the hashed filenames.
- Change one line, rebuild, and record which filenames changed.
- Run npm run preview and confirm the preview serves the new hashed bundle.
npm create vite@latest react-course-app -- --template react-ts
cd react-course-app
npm install
npm run dev
# ... build the CourseCard, then:
npm run build
ls dist dist/assetsShow one solutionHide solution
1. npm run dev serves the app on http://localhost:5173.
Saving CourseCard.tsx updates the page in place - no reload.
2. npm run build writes:
dist/index.html
dist/favicon.svg, dist/icons.svg from public/, unhashed
dist/assets/index-<hash>.js your code + React
dist/assets/index-<hash>.css
dist/assets/<image>-<hash>.png|svg imported images
3. After a one-line change to a component, only
dist/assets/index-<hash>.js gets a new name. The CSS and images
keep theirs, because their contents did not change.
4. npm run preview serves dist/ on http://localhost:4173, and
dist/index.html now references the new index-<hash>.js.
The contents of dist/ are what you upload to a host or CDN.Key points
- React is the UI library; Vite is the dev server and build tool around it.
- npm create vite@latest app -- --template react-ts scaffolds a project; the -- passes the flag through.
- npm install fills node_modules from package.json, for this project only.
- npm run dev serves src/ on localhost:5173 and updates the page on save without losing state.
- npm run build runs tsc -b, then vite build, producing dist/.
- Vite does not type-check - the dev server runs code with type errors; the build does not.
- dist/index.html points at hashed bundles; your TypeScript and JSX are not in dist.
- A hash changes only when that file content changes, which makes long-lived caching safe.
- public/ is copied unhashed; imported assets are hashed.
- npm run preview serves dist/ on localhost:4173. Deploy dist/, never src/.
Quick check before you move on
Interview questions
What is Vite?
A frontend development server and build tool. In development it serves source files on demand with Hot Module Replacement; for production it bundles the app into optimised, hashed static files.
What is the difference between React and Vite?
React is the UI library - components, state, rendering. Vite is the tooling that serves and builds a React app. You could build React with a different tool without changing your components.
What does npm run dev do?
It runs the dev script, vite, which starts a local server - by default on port 5173 - that compiles files on request and pushes changes to the browser as you save.
What does npm run build do?
In the React TypeScript template it runs tsc -b to type-check the project, then vite build to produce the dist/ folder. If the type check fails, the build stops.
Why does the dev server run code that has type errors?
Vite strips TypeScript types without checking them, which is what keeps it fast. Type checking is left to the editor and to the tsc step in the build script.
What is in dist/, and why do the filenames have hashes?
An index.html plus hashed JavaScript, CSS, and asset files, and anything copied from public/. The hash is derived from each file content, so a file is renamed only when it changes - which lets hosts cache assets for a long time and still serve new versions immediately.
Why should you not edit dist/ directly?
It is generated output, rebuilt from src/ on every build, so manual changes are overwritten. Change the source and rebuild.
Quiz
- 1.
What is Vite? A. A database B. A UI library C. A build tool and development server D. A programming language
- 2.
Which command starts the development server? A. npm start-react B. npm run dev C. npm run production D. vite production
- 3.
Which command creates the production build? A. npm run build B. npm run dev C. npm install build D. npm production
- 4.
Where does Vite normally place the production build? A. src/ B. node_modules/ C. dist/ D. public/src/
- 5.
Which statement is correct? A. React and Vite are the same thing B. Vite replaces React C. React is a UI library and Vite provides development and build tooling D. Vite is a database
- 6.
Code with a type error runs on npm run dev. What happens on npm run build? A. It builds anyway B. It fails at the tsc step C. It removes the error D. It switches to JavaScript
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned