Tool: Vite 6 · React 19 · JavaScript / JSX
Demo project:react-vite-counter
Table of Contents
- Why Vite for React?
- Module 1 — Getting Started with Vite and React
- Module 2 — Leveraging Hot Module Replacement and Builds
- Module 3 — Enhancing Workflow with Vite’s Modern Tooling
- Vite Architecture — Diagrams
- Reference Tables
- Testing with Vitest
- Best Practices
1. Why Vite for React?
Create React App (CRA) was long the go-to tool for starting a React project. It is now deprecated. Vite is its modern, faster, simpler successor.
CRA vs Vite Comparison
| Aspect | CRA / Webpack | Vite |
|---|---|---|
| Dev server startup | Several seconds | < 1 second |
| HMR | Slow, unreliable | Instant (Fast Refresh) |
| Dev bundler | Webpack (everything bundled) | Native ESM (no bundling) |
| Prod bundler | Webpack | Rollup |
| Dependency pre-bundling | No | esbuild (Go, 10–100× faster) |
| Base config | Complex | Minimal |
| TypeScript support | Manual configuration | Out of the box |
Module 1 — Getting Started with Vite and React
Creating a Vite + React Project
# Create a new interactive project
npm create vite@latest
# Or directly with the React template
npm create vite@latest react-vite-counter -- --template react
# Then initialize
cd react-vite-counter
npm install
npm run dev
Note: The
npm startcommand doesn’t work with Vite by default. Usenpm run dev.
Generated Project Structure
react-vite-counter/
├── public/
│ └── vite.svg # Static assets (accessible at root)
├── src/
│ ├── assets/
│ │ └── react.svg
│ ├── App.jsx # Root component
│ ├── App.css
│ ├── main.jsx # Entry point — React mounting in DOM
│ └── index.css
├── index.html # HTML template (Vite entry point)
├── vite.config.js # Vite configuration
├── package.json
└── eslint.config.js
src/main.jsx — 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>,
)
src/App.jsx — Basic Root Component
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
</>
)
}
export default App
package.json — Scripts and Dependencies
{
"name": "react-vite-counter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.3.1",
"vitest": "^3.1.2",
"jsdom": "^26.1.0",
"@testing-library/react": "^16.3.0",
"@testing-library/jest-dom": "^6.6.3"
}
}
Important:
"type": "module"indicates that all.jsfiles are treated as ES Modules.
Basic Configuration — vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()], // React plugin (Fast Refresh, JSX transform)
build: {
outDir: './dist' // Production build output folder
},
server: {
port: 8080,
host: 'localhost',
proxy: {
'/api': { // Proxy to avoid CORS issues in dev
target: 'http://localhost/',
changeOrigin: true,
secure: false,
rewrite: path => path.replace('/api', '')
}
}
},
test: { // Vitest configuration
environment: 'jsdom',
setupFiles: ['./src/test/setup.js'],
globals: true
}
})
Main defineConfig Fields
| Field | Description |
|---|---|
plugins | Array of Vite plugins (React, SVGR, visualizer, etc.) |
build | Production build options (outDir, minify, etc.) |
server | Dev server (port, host, proxy, HTTPS, etc.) |
test | Integrated Vitest configuration |
define | Global variables injected into the final build |
resolve.alias | Path aliases (e.g., @ → ./src) |
css | CSS configuration (modules, preprocessors) |
optimizeDeps | Dependency pre-bundling with esbuild |
Module 2 — Leveraging Hot Module Replacement and Builds
HMR and Fast Refresh
Hot Module Replacement (HMR) allows updating running code without fully reloading the page. Fast Refresh is the official React implementation supported by Vite.
sequenceDiagram
participant Dev as Developer
participant IDE as VS Code
participant Vite as Vite Dev Server
participant Browser as Browser
Dev->>IDE: Modifies App.jsx
IDE->>Vite: File save (fs event)
Vite->>Vite: Invalidate only the modified module
Vite->>Browser: Send HMR update via WebSocket
Browser->>Browser: Hot replace the module
Note over Browser: Component state preserved ✓
Note over Browser: No full reload ✓
Fast Refresh Advantages vs Old Hot Reload
- Reliable: officially supported by the React core
- State preserved: component state remains intact during modifications
- Fast: only the modified module is updated
- Compatible: works with React 19’s
usehook and CSS Modules
Production Build
# Create a production build (Rollup)
npm run build
# Preview the production build locally
npm run preview
The production build uses Rollup (not esbuild) for advanced optimization: tree-shaking, minification, code splitting.
Important Build Options
export default defineConfig({
build: {
outDir: './dist', // Output folder
reportCompressedSize: true, // Show gzip size
minify: 'esbuild', // 'terser' | 'esbuild' | false
sourcemap: false, // Enable in staging
rollupOptions: {
output: {
manualChunks: { // Separate vendor libs
vendor: ['react', 'react-dom']
}
}
}
}
})
Code Splitting with React.lazy
Vite natively supports code splitting via ES2020 dynamic imports. React provides React.lazy + Suspense for lazy loading components.
// App.jsx
import { useState, lazy, Suspense } from 'react'
// Dynamic import → Vite generates a separate chunk
const HeaderComponent = lazy(() => import('./Header'))
export const App = () => {
const [count, setCount] = useState(0)
return (
<>
{/* Suspense shows fallback while loading */}
<Suspense fallback={<div>Loading...</div>}>
<HeaderComponent />
</Suspense>
<div className="card">
<button onClick={() => setCount(c => c + 1)}>
The new count is {count}
</button>
</div>
</>
)
}
// Header.jsx — will be in its own JS chunk
export const HeaderComponent = () => (
<header>
<h2>My React + Vite Application</h2>
</header>
)
Bundle Analysis
To visualize bundle composition and identify heavy dependencies:
npm install -D rollup-plugin-visualizer
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
react(),
visualizer({
emitFile: true,
filename: 'stats.html' // Generated in dist/stats.html
})
]
})
After npm run build, open dist/stats.html in a browser to see the interactive bundle treemap.
Debugging Stale HMR Updates
| Symptom | Probable Cause | Solution |
|---|---|---|
| Component not refreshed | Anonymous default export | Use named exports |
| State reset | Logic modified outside component | Move logic inside component |
| No CSS update | Missing global CSS import | Check imports in main.jsx |
| HMR disabled | import.meta.hot not configured | Vite handles this automatically with React plugin |
// Best practice: named exports for better tree-shaking and HMR
export const MyComponent = () => { ... } // ✓ Named
export default function MyComponent() { ... } // ✓ Named with default
const MyComponent = () => { ... }
export default MyComponent // ✓ Acceptable
export default () => { ... } // ✗ Anonymous — less reliable HMR
Module 3 — Enhancing Workflow with Vite’s Modern Tooling
Vite Plugins
Plugin System Architecture
flowchart LR
CONFIG["vite.config.js\nplugins: [...]"]
P1["@vitejs/plugin-react\nJSX transform\nFast Refresh"]
P2["vite-plugin-svgr\nSVG → React components"]
P3["rollup-plugin-visualizer\nBundle analysis"]
P4["Custom plugin\nvia createPlugin()"]
CONFIG --> P1 & P2 & P3 & P4
P1 --> DEV["Dev Server\n(ESM)"]
P1 --> PROD["Prod Build\n(Rollup)"]
P2 --> DEV & PROD
Example: vite-plugin-svgr — SVG as React Components
npm install -D vite-plugin-svgr
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import svgr from 'vite-plugin-svgr'
export default defineConfig({
plugins: [
react(),
svgr()
]
})
// Before: SVG as image
import reactLogo from './assets/react.svg'
<img src={reactLogo} className="logo react" alt="React logo" />
// After: SVG as React component (via ?react)
import Logo from './assets/react.svg?react'
<Logo className="logo react" />
Popular Plugins for React + Vite
| Plugin | Usage | Installation |
|---|---|---|
@vitejs/plugin-react | JSX, Fast Refresh (Babel) | npm i -D @vitejs/plugin-react |
@vitejs/plugin-react-swc | JSX, Fast Refresh (SWC — faster) | npm i -D @vitejs/plugin-react-swc |
vite-plugin-svgr | Import SVG as React components | npm i -D vite-plugin-svgr |
rollup-plugin-visualizer | Bundle visualization | npm i -D rollup-plugin-visualizer |
vite-plugin-pwa | Progressive Web App | npm i -D vite-plugin-pwa |
@vitejs/plugin-legacy | Legacy browser support | npm i -D @vitejs/plugin-legacy |
vite-tsconfig-paths | Path aliases from tsconfig | npm i -D vite-tsconfig-paths |
Environment Variables
Vite manages environment variables via .env files. Any variable exposed to the client must have the VITE_ prefix.
.env Files and Their Priority
.env # Default (always loaded)
.env.local # Default local (not committed)
.env.development # Loaded in dev mode (npm run dev)
.env.development.local
.env.production # Loaded in prod mode (npm run build)
.env.production.local
# .env
VITE_APP_NAME=MyApp
VITE_API_KEY=abc123
VITE_API_URL=https://api.example.com
# Variables without VITE_: server-side only (not exposed to client)
DATABASE_URL=postgres://localhost/mydb
SECRET_KEY=super-secret
// src/EnvDisplay.jsx — Client-side variable access
const EnvDisplay = () => {
const apiKey = import.meta.env.VITE_API_KEY
const appName = import.meta.env.VITE_APP_NAME
const apiUrl = import.meta.env.VITE_API_URL
const mode = import.meta.env.MODE // 'development' | 'production'
return (
<div className="space-y-4">
<h2 className="text-lg font-semibold">Environment Details</h2>
<div className="bg-gray-50 p-4 rounded-md text-black">
<p><strong>App Name:</strong> {appName || 'Not set'}</p>
<p><strong>API Key:</strong> {apiKey || 'Not set'}</p>
<p><strong>API URL:</strong> {apiUrl || 'Not set'}</p>
<p><strong>Mode:</strong> {mode}</p>
</div>
<p className="text-sm text-gray-600">
⚠️ Never expose sensitive keys in production.
</p>
</div>
)
}
export default EnvDisplay
Predefined import.meta.env Variables
| Variable | Description | Example |
|---|---|---|
import.meta.env.MODE | Current mode | 'development' |
import.meta.env.BASE_URL | Base URL (base in config) | '/' |
import.meta.env.PROD | true in production mode | false |
import.meta.env.DEV | true in development mode | true |
import.meta.env.SSR | true for server-side rendering | false |
import.meta.env.VITE_* | Custom variables | 'abc123' |
Styling with Tailwind CSS and PostCSS
Vite integrates seamlessly with PostCSS and Tailwind CSS. No modification to vite.config.js is needed — Vite automatically detects postcss.config.js.
npm install -D tailwindcss @tailwindcss/postcss postcss
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
}
// postcss.config.js
export default {
plugins: {
'@tailwindcss/postcss': {}
}
}
/* src/index.css */
@import 'tailwindcss';
@tailwind base;
@tailwind components;
@tailwind utilities;
Vite Architecture — Diagrams
Vite Build Pipeline (Dev vs Production)
flowchart TB
subgraph DEV["DEV MODE — npm run dev"]
direction LR
D1["index.html\n(entry point)"]
D2["esbuild\n(pre-bundle\nnpm dependencies)"]
D3["Dev Server\n(native ESM)"]
D4["Browser\n(loads via ESM)"]
D5["HMR WebSocket"]
D1 --> D3
D2 --> D3
D3 -->|"Serve modules\non demand"| D4
D4 <-->|"Fast Refresh\nupdates"| D5
D5 --> D3
end
subgraph PROD["PROD MODE — npm run build"]
direction LR
P1["Source Files\n(JSX, CSS, SVG...)"]
P2["Rollup\n(tree-shaking\ncode splitting)"]
P3["Minification\n(esbuild/terser)"]
P4["dist/\n(hashed chunks)"]
P1 --> P2 --> P3 --> P4
end
Reference Tables
Vite CLI Commands
| Command | Description |
|---|---|
npm create vite@latest | Create a new interactive project |
npm run dev | Start dev server (port 5173 by default) |
npm run build | Production build in dist/ |
npm run preview | Preview the production build |
npm run lint | ESLint linter |
npm run test | Run Vitest |
vite --port 3000 | Dev server on a specific port |
vite build --mode staging | Build with a custom mode |
Complete build Configuration
| Option | Default | Description |
|---|---|---|
outDir | 'dist' | Output folder |
assetsDir | 'assets' | Sub-folder for assets |
sourcemap | false | Generate source maps |
minify | 'esbuild' | Minify JS ('terser' | 'esbuild' | false) |
reportCompressedSize | true | Show gzip size |
chunkSizeWarningLimit | 500 | Alert if chunk > N kB |
target | 'modules' | ES target (e.g., 'es2020') |
rollupOptions | {} | Advanced Rollup options |
Complete server Configuration
| Option | Default | Description |
|---|---|---|
port | 5173 | Dev server port |
host | 'localhost' | Listening address |
open | false | Automatically open browser |
proxy | {} | API request proxy |
https | false | Enable HTTPS |
cors | false | Enable CORS |
hmr | true | Enable/configure HMR |
Testing with Vitest
Vitest is Vite’s native testing tool. It shares the same configuration and supports JSX transformations natively.
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom
// vite.config.js — Add test configuration
export default defineConfig({
// ... other config ...
test: {
environment: 'jsdom', // Simulate browser DOM
setupFiles: ['./src/test/setup.js'],
globals: true // No need to import describe/it/expect
}
})
// src/test/setup.js
import '@testing-library/jest-dom'
// App.test.jsx
import { render, screen, fireEvent } from '@testing-library/react'
import { App } from '../App'
describe('App Component', () => {
it('renders initial count of 0', () => {
render(<App />)
expect(screen.getByText(/count is 0/i)).toBeInTheDocument()
})
it('increments count on button click', () => {
render(<App />)
const button = screen.getByRole('button')
fireEvent.click(button)
expect(screen.getByText(/count is 1/i)).toBeInTheDocument()
})
})
# Run tests
npm run test
# Run in watch mode
npx vitest
# Run with UI
npx vitest --ui
Best Practices
| Practice | Why |
|---|---|
Use npm run dev not npm start | Vite’s standard command |
| Named exports for components | Better tree-shaking and HMR |
Prefix env vars with VITE_ | Only VITE_ vars exposed to client |
Never commit .env.local | Add to .gitignore |
Use React.lazy for large components | Smaller initial bundle |
| Analyze bundle with visualizer | Identify performance bottlenecks |
Use vite preview before deploy | Test production behavior locally |
| Pin Vite version in package.json | Avoid unexpected breaking changes |
Search Terms
vite · react · typescript · frontend · development · configuration · architecture · fast · hmr · hot · plugins · production · refresh · src · variables