← Back to React
Lesson 4 · React Fundamentals

Creating a React App with Vite

Scaffolding, the dev server, and what the build actually produces.

Beginner30 min

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.

Who does what
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.

The four scripts
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.

A real dist/ from the default template
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

Create, install, and run
# 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:5173
What the scaffold creates (create-vite 9, react-ts)
my-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
package.json, as generated
{ "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" } }
vite.config.ts, as generated - one plugin is all React needs
import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], })
main.tsx, as generated
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>, )
The dev server starting
$ npm run dev VITE v8.3.1 ready in 86 ms ➜ Local: http://localhost:5173/
A first component, and using it
// 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;
The build, from a real run
$ 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
dist/index.html - rewritten by the build
<!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>
One edit, one new filename
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.
The type check lives in the build
// 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 produced
Build, then preview what you will deploy
npm 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@latest

Scaffolds a new project from a template.

npm create vite@latest app -- --template react-ts
npm install

Installs the project dependencies into node_modules/.

npm i
npm run dev

Starts the dev server with Hot Module Replacement.

http://localhost:5173
npm run build

Type-checks, then builds the production output.

tsc -b && vite build
npm run preview

Serves 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.

Keep the state

“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.”

Rename a hash

“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.”

Sneak a type error past dev

“Add const lessons: number = "192" to App.tsx. The dev server keeps working; npm run build fails. Fix it and build again.”

Read the build

“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

Thinking Vite is React

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.

Editing files in dist/

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 build
Committing node_modules or dist

Both are generated - node_modules from package-lock.json, dist from src/. The generated .gitignore already excludes both; keep it that way.

Deploying src/ or the dev server

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 CDN
Trusting the dev server to catch type errors

Vite 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.

Forgetting the -- in the create command

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-ts

Best 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.

1.

Scaffold a React + TypeScript project called react-course-app without answering any prompts, then start it.

Show hint

Pass the template on the command line - and remember the -- before it.

Show solution
npm create vite@latest react-course-app -- --template react-ts cd react-course-app npm install npm run dev
2.

Replace App with a heading "My React Course" and a CourseCard component in src/components/CourseCard.tsx.

Show hint

One file per component, exported as default, imported into App.

Show 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;
3.

Explain what each of the three scripts - dev, build, preview - is for, and which one produces something you deploy.

Show hint

Two of them start a server; only one of them writes files.

Show 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.
Coding challenge

From npm create to a deployable dist/

Take one project through the whole lifecycle, and prove to yourself what each step produced.

It should
  • 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/assets
Show one 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

Does Vite replace React?
No. React is the UI library; Vite serves it in development and builds it for production.
Your app has a type error but npm run dev works fine. Why?
Vite strips types without checking them. The type check is the tsc -b step at the start of npm run build.
You edit one component and rebuild. Which filenames in dist/assets change?
Only the JavaScript bundle. Files whose contents did not change, like the CSS and images, keep their names.
Which folder do you deploy?
dist/ - the build output. Not src/, and not the whole project.
What does the -- do in npm create vite@latest app -- --template react-ts?
It tells npm to stop reading flags, so --template is passed through to create-vite.

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. 1.

    What is Vite? A. A database B. A UI library C. A build tool and development server D. A programming language

  2. 2.

    Which command starts the development server? A. npm start-react B. npm run dev C. npm run production D. vite production

  3. 3.

    Which command creates the production build? A. npm run build B. npm run dev C. npm install build D. npm production

  4. 4.

    Where does Vite normally place the production build? A. src/ B. node_modules/ C. dist/ D. public/src/

  5. 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. 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...