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
    Vite: A Tool, Not a Framework
    Vite Quick Start
    Create a Project
    TypeScript Template
    Project Anatomy (high level)
    Core Concepts
    index.html as the entry
    React Mount
    Configuration Essentials
    vite.config.ts
    Environment Variables
    API Proxies & CORS in Dev
    Build & Performance
    Code Splitting & Dynamic Import
    Images & Static Assets
    Production Build & Preview
    SSR, SSG, and “Islands”
    Developer Experience Toolkit
    Testing with Vitest + React Testing Library
    Linting & Formatting
    Common Pitfalls (and Fixes)
    Example: Axios helper with base URL and interceptors
    Vite Plugin Picks for React
    Migration Tips (CRA → Vite)
    Conclusion

    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.

    FeatureGatsbyVite + React
    BundlerWebpack (built-in)Rollup (via Vite)
    RoutingFile-based (src/pages/)Bring-your-own (React Router, TanStack Router, etc.)
    Data LayerBuilt-in GraphQLNone
    Plugins/EcosystemRich, CMS/image/SEOGeneral-purpose Vite plugins (no CMS/data focus)
    RenderingSSG/SSR/DSG built-inClient-side by default; SSR requires manual setup
    Dev ExperienceGood (webpack optimized by Gatsby team)Excellent (native ESM, instant HMR)
    Use CaseContent-heavy static sites, blogs, docsFlexible 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) via process.env.* or loadEnv—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 write describe/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" in package.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 the parser 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)
    ^

    1. Move HTML: port public/index.html → project root index.html and update <script type="module" src="/src/main.jsx">.
    2. Entry: rename CRA’s src/index.tsx|jsx to main.* (or update index.html to point at the actual file).
    3. Env vars: rename to VITE_*.
    4. Aliases: recreate CRA aliases in vite.config.ts.
    5. Static assets: place in public/ or import from src/.
    6. 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.