Beginner

Using Vite with React

Why Vite for React — getting started, HMR, builds, modern tooling and testing with Vitest.

Tool: Vite 6 · React 19 · JavaScript / JSX
Demo project: react-vite-counter


Table of Contents

  1. Why Vite for React?
  2. Module 1 — Getting Started with Vite and React
  3. Module 2 — Leveraging Hot Module Replacement and Builds
  4. Module 3 — Enhancing Workflow with Vite’s Modern Tooling
  5. Vite Architecture — Diagrams
  6. Reference Tables
  7. Testing with Vitest
  8. 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

AspectCRA / WebpackVite
Dev server startupSeveral seconds< 1 second
HMRSlow, unreliableInstant (Fast Refresh)
Dev bundlerWebpack (everything bundled)Native ESM (no bundling)
Prod bundlerWebpackRollup
Dependency pre-bundlingNoesbuild (Go, 10–100× faster)
Base configComplexMinimal
TypeScript supportManual configurationOut 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 start command doesn’t work with Vite by default. Use npm 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 .js files 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

FieldDescription
pluginsArray of Vite plugins (React, SVGR, visualizer, etc.)
buildProduction build options (outDir, minify, etc.)
serverDev server (port, host, proxy, HTTPS, etc.)
testIntegrated Vitest configuration
defineGlobal variables injected into the final build
resolve.aliasPath aliases (e.g., @./src)
cssCSS configuration (modules, preprocessors)
optimizeDepsDependency 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 use hook 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

SymptomProbable CauseSolution
Component not refreshedAnonymous default exportUse named exports
State resetLogic modified outside componentMove logic inside component
No CSS updateMissing global CSS importCheck imports in main.jsx
HMR disabledimport.meta.hot not configuredVite 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" />
PluginUsageInstallation
@vitejs/plugin-reactJSX, Fast Refresh (Babel)npm i -D @vitejs/plugin-react
@vitejs/plugin-react-swcJSX, Fast Refresh (SWC — faster)npm i -D @vitejs/plugin-react-swc
vite-plugin-svgrImport SVG as React componentsnpm i -D vite-plugin-svgr
rollup-plugin-visualizerBundle visualizationnpm i -D rollup-plugin-visualizer
vite-plugin-pwaProgressive Web Appnpm i -D vite-plugin-pwa
@vitejs/plugin-legacyLegacy browser supportnpm i -D @vitejs/plugin-legacy
vite-tsconfig-pathsPath aliases from tsconfignpm 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

VariableDescriptionExample
import.meta.env.MODECurrent mode'development'
import.meta.env.BASE_URLBase URL (base in config)'/'
import.meta.env.PRODtrue in production modefalse
import.meta.env.DEVtrue in development modetrue
import.meta.env.SSRtrue for server-side renderingfalse
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

CommandDescription
npm create vite@latestCreate a new interactive project
npm run devStart dev server (port 5173 by default)
npm run buildProduction build in dist/
npm run previewPreview the production build
npm run lintESLint linter
npm run testRun Vitest
vite --port 3000Dev server on a specific port
vite build --mode stagingBuild with a custom mode

Complete build Configuration

OptionDefaultDescription
outDir'dist'Output folder
assetsDir'assets'Sub-folder for assets
sourcemapfalseGenerate source maps
minify'esbuild'Minify JS ('terser' | 'esbuild' | false)
reportCompressedSizetrueShow gzip size
chunkSizeWarningLimit500Alert if chunk > N kB
target'modules'ES target (e.g., 'es2020')
rollupOptions{}Advanced Rollup options

Complete server Configuration

OptionDefaultDescription
port5173Dev server port
host'localhost'Listening address
openfalseAutomatically open browser
proxy{}API request proxy
httpsfalseEnable HTTPS
corsfalseEnable CORS
hmrtrueEnable/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

PracticeWhy
Use npm run dev not npm startVite’s standard command
Named exports for componentsBetter tree-shaking and HMR
Prefix env vars with VITE_Only VITE_ vars exposed to client
Never commit .env.localAdd to .gitignore
Use React.lazy for large componentsSmaller initial bundle
Analyze bundle with visualizerIdentify performance bottlenecks
Use vite preview before deployTest production behavior locally
Pin Vite version in package.jsonAvoid unexpected breaking changes

Search Terms

vite · react · typescript · frontend · development · configuration · architecture · fast · hmr · hot · plugins · production · refresh · src · variables

Interested in this course?

Contact us to book it or get a custom training plan for your team.