Vite React
September 30, 2025
Overview
When you first hear “Vite React,” it can sound like just another React-based framework, something akin to Gatsby or Next.js. But the comparison isn’t one-to-one. Vite is not a framework — it’s a modern build tool and dev server. To use Vite with React means you’re building React apps with a very fast, lean bundling system.
Let’s break down what that means, especially compared to something like Gatsby.
Table of Contents
Gatsby vs Vite
Gatsby: More Than a Bundler
At its core, Gatsby historically relied on Webpack to bundle JavaScript. But Gatsby doesn’t stop there — it adds a whole opinionated framework layer:
- Routing: file-based, every file in
src/pages/
becomes a route. - Data layer: a GraphQL API that unifies data from Markdown, headless CMSs, APIs, and local files.
- Plugins & ecosystem: prebuilt solutions for SEO, image optimization, analytics, etc.
- Rendering model: static site generation (SSG) with options for SSR and deferred builds.
So while Webpack is “just” the bundler, Gatsby orchestrates the entire lifecycle of a site — from data sourcing to page rendering to static asset optimization.
Vite: A Tool, Not a Framework
Vite focuses narrowly on the build process:
- Lightning-fast dev server with native ES modules.
- HMR (Hot Module Reloading) so changes reflect instantly.
- Rollup-based production builds with tree-shaking and code-splitting.
- Plugin support (e.g.,
@vitejs/plugin-react-swc
) for JSX/TSX transforms.
That’s it. Vite doesn’t give you routing, data pipelines, or opinionated plugins for SEO/images. It assumes you will bring those, either by writing them yourself or by combining Vite with other libraries.
Feature | Gatsby | Vite + React |
---|---|---|
Bundler | Webpack (built-in) | Rollup (via Vite) |
Routing | File-based (src/pages/ ) | Bring-your-own (React Router, TanStack Router, etc.) |
Data Layer | Built-in GraphQL | None |
Plugins/Ecosystem | Rich, CMS/image/SEO | General-purpose Vite plugins (no CMS/data focus) |
Rendering | SSG/SSR/DSG built-in | Client-side by default; SSR requires manual setup |
Dev Experience | Good (webpack optimized by Gatsby team) | Excellent (native ESM, instant HMR) |
Use Case | Content-heavy static sites, blogs, docs | Flexible React apps, SPAs, or custom frameworks |
Vite Quick Start
Create a Project
Start from a clean template so you can see Vite’s instant dev server and React Fast Refresh with zero config.
# npm 7+
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev
This scaffolds a React app, installs deps, and launches the dev server with hot updates.
TypeScript Template
Prefer TypeScript? Use the TS template—same DX, typed from the start.
npm create vite@latest my-app -- --template react-ts
Project Anatomy (high level)
Vite treats your HTML as the real entry and loads modules on demand—here’s the minimum you need to know.
my-app/
index.html # HTML entry (script type="module")
src/
main.jsx # Vite entry for React (can also be .tsx)
App.jsx
assets/
vite.config.ts|js # Vite + plugin config
Core Concepts
index.html as the entry
Unlike older toolchains, you own the HTML. That means simpler control over meta tags, fonts, and scripts.
<!-- index.html -->
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Vite React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
React Mount
React attaches to the #root
element the HTML provides. This is the smallest possible bootstrap.
// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './styles.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Configuration Essentials
vite.config.ts
Most apps can skip config at first. Add options only when they solve a real problem (proxy, aliases, build tweaks).
// vite.config.ts
import { defineConfig, splitVendorChunkPlugin } from 'vite';
import react from '@vitejs/plugin-react-swc';
export default defineConfig({
plugins: [react(), splitVendorChunkPlugin()],
server: {
port: 5173,
open: true,
proxy: {
// proxy /api to backend during dev
'/api': { target: 'http://localhost:3000', changeOrigin: true }
}
},
resolve: {
alias: {
'@': '/src'
}
},
build: {
sourcemap: true
},
optimizeDeps: {
// Pre-bundle very large ESM deps to speed up dev server & HMR
include: ['lodash-es', 'date-fns'], // example; list your heavy deps
// If a package breaks Vite's pre-bundle, exclude it:
// exclude: ['some-esm-package']
}
});
- Plugins:
@vitejs/plugin-react-swc
enables Fast Refresh;splitVendorChunkPlugin
improves shared caching. - Server: The
/api
proxy avoids CORS during local dev. - Alias:
@ → /src
shortens imports (e.g.,@/components/Button
). - Build:
sourcemap: true
aids prod debugging. - optimizeDeps: Pre-bundles heavy deps to keep HMR snappy.
Environment Variables
- Files:
.env
,.env.development
,.env.production
- Env vars must be prefixed with
VITE_
to be exposed to client code. - Built-ins:
import.meta.env.MODE
,DEV
,PROD
,BASE_URL
. - Secrets: do not prefix with
VITE_
if they must not reach the browser. Read them only in Node contexts (e.g.vite.config.ts
, SSR handlers, CI) viaprocess.env.*
orloadEnv
—never in client-side modules.
# .env
VITE_API_BASE=/api
// src/lib/api.ts
export const API_BASE = import.meta.env.VITE_API_BASE;
API Proxies & CORS in Dev
Use Vite’s server.proxy
to avoid CORS headaches during development.
// vite.config.ts (excerpt)
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
ws: true, // enable if your backend uses WebSockets
secure: false // allow self-signed certs in local HTTPS dev
}
}
}
In production, the frontend should call the API behind the same origin or via a reverse proxy (NGINX, CDN rules).
Build & Performance
Code Splitting & Dynamic Import
Vite outputs separate chunks automatically for dynamic imports.
// lazy load a heavy page
import React, { Suspense, lazy } from 'react';
const ReportsPage = lazy(() => import('./pages/ReportsPage'));
export default function App() {
return (
<Suspense fallback={<div>Loading…</div>}>
<ReportsPage />
</Suspense>
);
}
Dynamic import()
tells Vite/Rollup to put ReportsPage
in its own file so the initial bundle stays small. The code for
that page is only fetched when the component is actually rendered (e.g., when a user navigates there). React.lazy
wires
the import to React, and <Suspense>
shows the fallback
while the chunk loads. Vite auto-creates the extra chunk and preloads
dependencies; you don’t need extra config. For lots of pages, consider import.meta.glob()
to lazy-map an entire folder. Notes: use
an error boundary for failed loads, keep the import()
path mostly static (add /* @vite-ignore */
only for truly dynamic paths), and
remember that SSR needs matching lazy/suspense support.
Images & Static Assets
- Import as URLs:
import logoUrl from './logo.svg?url'
- Import as ReactComponent (SVGR) via plugin:
npm i -D vite-plugin-svgr
// vite.config.ts
import svgr from 'vite-plugin-svgr';
plugins: [react(), svgr()];
import Logo from './logo.svg?react';
<Logo className="w-8 h-8" />
Production Build & Preview
npm run build # creates dist/
npm run preview # serves the production build locally
Deploy dist/
to any static host (Netlify, Vercel, S3+CloudFront, GitHub Pages). For routes using client-side navigation (React Router), enable SPA fallback to index.html
.
If deploying under a subpath (e.g., GitHub Pages user.github.io/repo
), set:
// vite.config.ts
export default defineConfig({ base: '/repo/' });
SSR, SSG, and “Islands”
Vite itself is a build tool; SSR is provided via framework integrations (e.g., Vite + React Router SSR, TanStack Start, Astro for islands, Next.js uses its own toolchain). If you need RSC or advanced SSR features, consider a framework on top; otherwise Vite is superb for SPAs and static-first apps.
Developer Experience Toolkit
Testing with Vitest + React Testing Library
Vitest is Vite’s test runner—it boots with your Vite config, so TS, JSX, aliases, and plugins “just work.” We add React Testing
Library for ergonomic component tests, jsdom
to simulate the browser, and @testing-library/jest-dom
for nicer matchers (like .toBeInTheDocument()
).
npm i -D vitest @testing-library/react jsdom @testing-library/jest-dom
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react-swc';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
globals: true, // optional: use global describe/it/expect
css: true // only if components import CSS files
}
});
npm i -D …
installs the runner (vitest
), DOM shim (jsdom
), and testing-library bits.plugins: [react()]
ensures the same React/SWC/Babel behavior as your app.environment: 'jsdom'
gives components a browser-like DOM; without it, tests run in Node.setupFiles
loads once before tests—perfect for extending matchers via@testing-library/jest-dom
.globals: true
lets you writedescribe/it/expect
without importing them.css: true
allows importing.css
in components under test (useful for CSS Modules).
// src/test/setup.ts
import '@testing-library/jest-dom';
// src/App.test.tsx
import { render, screen } from '@testing-library/react';
import App from './App';
it('renders title', () => {
render(<App />);
expect(screen.getByText(/Vite React/i)).toBeInTheDocument();
});
The sample test renders
<App />
and asserts on text—your “is the thing on the page?” smoke test.
npm run test
(Configure
"test": "vitest"
inpackage.json
.)
Linting & Formatting
npm i -D eslint @eslint/js eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-import @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-config-prettier
eslint
/@eslint/js
: core linter + base JS rules.eslint-plugin-react
/eslint-plugin-react-hooks
: React- and hooks-specific best practices.eslint-plugin-import
: sanity checks for imports (duplicates, unresolved paths, ordering).@typescript-eslint/parser
/@typescript-eslint/eslint-plugin
: TypeScript syntax + rules (even if you only have a few.ts/.tsx
files).prettier
/eslint-config-prettier
: Prettier formats code; the config disables conflicting ESLint formatting rules.
// .eslintrc.json
{
"extends": ["plugin:react/recommended", "plugin:react-hooks/recommended", "prettier"],
"plugins": ["react", "react-hooks", "@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"parserOptions": { "ecmaVersion": "latest", "sourceType": "module" },
"settings": { "react": { "version": "detect" } },
"rules": { "no-console": "warn" }
}
extends
:plugin:react/recommended
→ common React rules (prop types, JSX best practices).plugin:react-hooks/recommended
→ enforces the Rules of Hooks.prettier
→ turns off ESLint rules that would fight Prettier.
plugins
: Adds React, Hooks, and TS-specific rule sets.parser
:@typescript-eslint/parser
lets ESLint parse.ts/.tsx
(even if your project is mixed JS/TS).parserOptions
: Modern JS syntax + ES modules.settings.react.version: "detect"
: Auto-detects your installed React version for the right rules.rules.no-console: "warn"
: Console calls won’t fail CI, but you’ll see a warning.
JS-only project? You can drop
@typescript-eslint/*
and theparser
line.
// package.json scripts
{
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
"format": "prettier -w ."
}
One command to lint, one to format—easy to wire into CI and pre-commit hooks.
Common Pitfalls (and Fixes)
Pitfall | Why it happens | Fix |
---|---|---|
Environment vars not available | Vars must be prefixed with VITE_ to be exposed to client code | Rename to VITE_* , restart dev server |
Broken deep links in production | Static hosting returns 404 on client-side routes | Enable SPA fallback to index.html (e.g., “try files”/rewrite) on your host/CDN |
Slow dev when importing huge libs | ESM transforms on very large modules | Pre-bundle via optimizeDeps ; consider dynamic import |
SVG imports not as React components | Vite treats SVGs as assets by default | Use vite-plugin-svgr and import via ?react (or the named import pattern) |
Example: Axios helper with base URL and interceptors
// src/lib/http.ts
import axios from 'axios';
export const http = axios.create({
baseURL: import.meta.env.VITE_API_BASE || '/api',
withCredentials: true
});
http.interceptors.response.use(
(r) => r,
(err) => {
if (err.response?.status === 401) {
// handle auth (redirect, refresh, etc.)
}
return Promise.reject(err);
}
);
Usage:
import { http } from '@/lib/http';
const { data } = await http.get('/users');
Vite Plugin Picks for React
Plugin | What it adds | Notes |
---|---|---|
@vitejs/plugin-react-swc | Fast Refresh, JSX via SWC | Default (fast, great DX) |
@vitejs/plugin-react | Fast Refresh, Babel-based JSX | Use when you rely on specific Babel transforms/plugins (SWC may not cover them) |
vite-plugin-svgr | Import SVGs as React components | Use ?react query; great for icons/logos |
vite-tsconfig-paths | Resolves TS paths aliases | Pairs with @ → /src alias |
rollup-plugin-visualizer | Bundle analysis HTML report | Run post-build to inspect chunk sizes |
Migration Tips (CRA → Vite)
- Move HTML: port
public/index.html
→ project rootindex.html
and update<script type="module" src="/src/main.jsx">
. - Entry: rename CRA’s
src/index.tsx|jsx
tomain.*
(or updateindex.html
to point at the actual file). - Env vars: rename to
VITE_*
. - Aliases: recreate CRA aliases in
vite.config.ts
. - Static assets: place in
public/
or import fromsrc/
. - Testing: switch Jest → Vitest (often minimal code changes).
Conclusion
Vite gives React projects blazing dev velocity and optimized builds with minimal configuration. It’s ideal for SPAs and static deployments, and it integrates cleanly with modern tooling (Vitest, Tailwind, TS). If you need heavy SSR/RSC, pick a framework on top; otherwise, Vite + React is a fast, ergonomic default.