Episode 2 — React Frontend Architecture NextJS / 2.1 — Introduction to React

2.1.e — Setting Up React With Vite

In one sentence: Vite is a next-generation build tool that provides instant dev server startup and lightning-fast Hot Module Replacement by leveraging native ES modules during development and Rollup for optimized production builds.

Navigation: ← Real DOM vs Virtual DOM · Next → Project Structure Breakdown


1. Why NOT Create React App (CRA) in 2024+

Create React App was the official React project scaffolding tool from 2016 to 2023. It was deprecated and is no longer recommended.

What Happened to CRA

CRA Timeline:
  2016 — Released by Facebook. Revolutionary: zero-config React setup.
  2017-2019 — The default way to start React projects. Loved.
  2020 — Starts feeling slow. Alternative tools emerge (Vite, Snowpack).
  2021 — Updates slow down. Community concerns grow.
  2022 — Effectively unmaintained. Last significant update.
  2023 — React team officially recommends frameworks (Next.js, Remix) 
         or Vite instead. CRA repository archived.
  2024+ — Using CRA in a new project is actively discouraged.

Why CRA Became Problematic

CRA architecture:
  
  Your code ─────> Webpack ─────> Bundle ─────> Browser
                     |
                   (config hidden via react-scripts)
                     |
                   SLOW. VERY SLOW.
ProblemDetail
Slow startupDev server takes 30-60 seconds for large projects
Slow HMRChanges take 2-10 seconds to reflect
No config accessWebpack config hidden inside react-scripts
Ejecting is permanentnpm run eject spills 1000+ lines of config
Heavy dependenciesnode_modules bloat from Webpack ecosystem
Stale Webpack versionStuck on Webpack 5 with slow updates
No SSR supportCannot do server-side rendering
No code splitting defaultsMust configure manually
Security vulnerabilitiesUnmaintained dependencies accumulate CVEs
Dev server startup time comparison:

  CRA (Webpack):    ████████████████████████████████  30-60s
  Vite:             ██                                 1-2s
  
  HMR (Hot Module Replacement):
  CRA (Webpack):    ████████████  2-10s
  Vite:             █             <100ms

2. What Is Vite

Vite (French for "fast", pronounced /vit/) is a build tool created by Evan You (creator of Vue.js) in 2020. Despite its Vue origins, Vite is framework-agnostic and is the recommended tool for React projects.

Vite's Core Innovation

Vite uses two different strategies for development and production:

DEVELOPMENT: Native ES Modules (no bundling)

  Browser requests main.jsx
       |
       v
  Vite serves main.jsx as ES module
  Browser parses imports:
    import App from './App.jsx'
       |
       v
  Browser requests App.jsx
  Vite transforms it on-the-fly (JSX → JS)
  Serves it immediately
       |
       v
  Browser parses App's imports:
    import Header from './components/Header.jsx'
    import './App.css'
       |
       v
  Browser requests each import individually
  Vite transforms and serves each one
  
  KEY INSIGHT: Only the files you actually import are processed.
  If you have 500 files but only import 20 for the current page,
  only 20 files are processed.


PRODUCTION: Rollup bundling (optimized output)

  All source files ──> Rollup ──> Optimized bundles
                                    |
                                    +── index-abc123.js (main bundle)
                                    +── vendor-def456.js (React, etc.)
                                    +── about-ghi789.js (lazy chunk)
                                    +── style-jkl012.css

How Vite's Dev Server Differs from Webpack

Webpack dev server (CRA):
  
  Start dev server:
    1. Read ALL source files
    2. Build dependency graph (ALL files)
    3. Transform ALL files (Babel, PostCSS, etc.)
    4. Bundle EVERYTHING into memory
    5. Serve bundle to browser
    
  Time: 30-60 seconds (scales with project size)
  
  On file change:
    1. Determine what changed
    2. Re-bundle affected modules
    3. Send update to browser via WebSocket
    
  Time: 2-10 seconds


Vite dev server:
  
  Start dev server:
    1. Pre-bundle node_modules (esbuild, ~1 second, cached)
    2. Start HTTP server
    3. Done.
    
  Time: 1-2 seconds (constant, regardless of project size)
  
  On file change:
    1. Invalidate the changed module
    2. Browser re-requests just that module
    3. Vite transforms just that one file
    
  Time: <100ms (constant, regardless of project size)

Why Vite Is Fast: esbuild

Vite uses esbuild for dependency pre-bundling. esbuild is written in Go (not JavaScript) and is 10-100x faster than JavaScript-based bundlers:

Bundling speed comparison (bundling React + dependencies):

  Webpack (JavaScript):   ████████████████████  ~20 seconds
  Rollup (JavaScript):    ████████████████      ~16 seconds
  Parcel (JavaScript):    ███████████           ~11 seconds
  esbuild (Go):           █                     ~0.4 seconds
  
  esbuild is 25-50x faster because:
  - Written in Go (compiled, parallelized)
  - Uses all CPU cores
  - Minimal memory allocation
  - Single-pass architecture

3. Installation Walkthrough

Prerequisites

You need Node.js installed (version 18+):

# Check Node.js version
node --version
# Should show v18.x.x or v20.x.x or higher

# Check npm version
npm --version
# Should show 9.x.x or higher

Creating a New React Project

# The command to create a new Vite project:
npm create vite@latest

# This runs the create-vite package
# It will prompt you with questions:

#   ? Project name: > my-react-app
#   ? Select a framework:
#       Vanilla
#       Vue
#     > React
#       Preact
#       Lit
#       Svelte
#       Solid
#       Qwik
#       Angular
#       Others
#   ? Select a variant:
#     > TypeScript
#       TypeScript + SWC
#       JavaScript
#       JavaScript + SWC
#       Remix ↗

You can also skip the prompts by specifying the template:

# JavaScript + React
npm create vite@latest my-react-app -- --template react

# TypeScript + React
npm create vite@latest my-react-app -- --template react-ts

# TypeScript + React + SWC (faster builds)
npm create vite@latest my-react-app -- --template react-swc-ts

# JavaScript + React + SWC
npm create vite@latest my-react-app -- --template react-swc

After Creation

# Navigate into the project
cd my-react-app

# Install dependencies
npm install

# Start the development server
npm run dev

# Output:
#   VITE v5.x.x  ready in 300 ms
#
#   ➜  Local:   http://localhost:5173/
#   ➜  Network: use --host to expose
#   ➜  press h + enter to show help

The dev server starts in ~300ms. Open http://localhost:5173 in your browser.

Step-by-Step: What Happens When You Run npm create vite@latest

1. npm downloads the `create-vite` package (temporarily)
2. create-vite runs its CLI
3. You answer the prompts (or flags provide answers)
4. create-vite copies template files into the project directory:
   - package.json (with dependencies listed)
   - vite.config.js (Vite configuration)
   - index.html (the SPA entry point)
   - src/ folder (application code)
   - public/ folder (static assets)
5. Template files are written to disk
6. You run `npm install` to download dependencies
7. You run `npm run dev` to start the dev server

4. Available Templates

Vite offers several React templates. Each is a combination of language and compiler:

TemplateLanguageJSX CompilerUse Case
reactJavaScriptBabelSimple projects, learning
react-tsTypeScriptBabelProduction projects (recommended)
react-swcJavaScriptSWCPerformance-focused, Babel alternative
react-swc-tsTypeScriptSWCFastest TypeScript option

Babel vs SWC

Babel (traditional):
  - Written in JavaScript
  - Huge plugin ecosystem
  - Slower transformation
  - More mature, more plugins available
  
SWC (modern):
  - Written in Rust
  - 20-70x faster than Babel
  - Growing plugin ecosystem
  - Used by Next.js, Deno, and others
  - Recommended for new projects

Transformation speed:
  Babel:  ████████████████████  ~700ms for 1000 files
  SWC:    █                     ~30ms for 1000 files

Recommendation

For new projects: react-swc-ts (TypeScript + SWC)

  • TypeScript catches bugs at compile time
  • SWC is faster than Babel
  • The industry standard for production React projects

For learning and experimenting: react (JavaScript)

  • No TypeScript complexity while learning React concepts
  • Fewer files and configuration to understand

5. Understanding vite.config.js

The Vite configuration file controls how Vite behaves:

// vite.config.js (default for React template)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
});

That's the minimum. Here's a fully annotated version with common options:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  // PLUGINS
  // Vite plugins extend functionality
  plugins: [
    react({
      // Use automatic JSX runtime (no need to import React)
      jsxRuntime: 'automatic',
      // Babel plugins (only if using @vitejs/plugin-react, not SWC)
      babel: {
        plugins: [
          // Add Babel plugins here if needed
        ],
      },
    }),
  ],

  // DEV SERVER
  server: {
    port: 3000,           // Default is 5173
    open: true,            // Open browser on start
    host: true,            // Listen on all network interfaces
    strictPort: true,      // Fail if port is in use (vs auto-incrementing)
    
    // Proxy API requests to backend during development
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        // rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },

  // BUILD
  build: {
    outDir: 'dist',        // Output directory (default: dist)
    sourcemap: true,       // Generate source maps for debugging
    minify: 'esbuild',    // Minifier: 'esbuild' (fast) or 'terser' (smaller)
    target: 'es2015',      // Browser compatibility target
    
    rollupOptions: {
      output: {
        // Custom chunk splitting
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom'],
        },
      },
    },
  },

  // RESOLVE
  resolve: {
    alias: {
      // Path aliases: import from '@/components/Button' 
      // instead of '../../components/Button'
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@hooks': path.resolve(__dirname, './src/hooks'),
      '@utils': path.resolve(__dirname, './src/utils'),
    },
  },

  // CSS
  css: {
    modules: {
      localsConvention: 'camelCase',  // .my-class → styles.myClass
    },
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`,
      },
    },
  },

  // PREVIEW (production preview server)
  preview: {
    port: 4173,
    open: true,
  },

  // DEFINE (compile-time constants)
  define: {
    __APP_VERSION__: JSON.stringify('1.0.0'),
  },
});

Key Configuration Sections

vite.config.js structure:

  plugins:    Extensions that add functionality (React, PWA, etc.)
  server:     Dev server settings (port, proxy, CORS)
  build:      Production build settings (output, minification, chunks)
  resolve:    Module resolution (aliases, extensions)
  css:        CSS processing (modules, preprocessors)
  preview:    Production preview server settings
  define:     Compile-time constant replacements
  optimizeDeps: Dependency pre-bundling configuration

6. Dev Server: npm run dev

When you run npm run dev, here's what happens under the hood:

$ npm run dev

[1] npm reads package.json scripts.dev → "vite"
[2] Vite starts

[3] PRE-BUNDLING (first run only, cached after):
    Vite scans your code for bare imports (like 'react', 'react-dom')
    Uses esbuild to bundle node_modules into optimized chunks
    Stores them in node_modules/.vite/
    
    Why? node_modules has thousands of files with CommonJS modules.
    Browsers can't import CommonJS. Pre-bundling converts them to
    ES modules and consolidates them (react has 30+ files → 1 file).
    
    This runs once and is cached until dependencies change.

[4] HTTP SERVER starts on http://localhost:5173
    
[5] Browser requests http://localhost:5173/
    Vite serves index.html:
    
    <script type="module" src="/src/main.jsx"></script>
    
    The type="module" tells the browser this is an ES module.

[6] Browser requests /src/main.jsx
    Vite transforms it on-the-fly:
    - JSX → JavaScript (via Babel or SWC)
    - Import paths resolved
    - Served with proper Content-Type header

[7] Browser parses main.jsx, finds imports:
    import App from './App.jsx'
    import './index.css'
    
    Browser requests each import

[8] Process repeats for each imported file:
    File requested → Vite transforms → Browser receives → parses imports
    
    This is the "unbundled" development model.
    Only imported files are processed. Unused files are ignored.

What the Browser Network Tab Shows

Network requests during Vite dev:

  GET /                       → index.html
  GET /src/main.jsx           → transformed main.jsx (ES module)
  GET /src/App.jsx            → transformed App.jsx
  GET /src/App.css            → CSS (injected via JS)
  GET /src/components/Header.jsx → transformed Header.jsx
  GET /src/components/Footer.jsx → transformed Footer.jsx
  GET /@react-refresh         → Vite's HMR client
  GET /node_modules/.vite/deps/react.js → pre-bundled React
  GET /node_modules/.vite/deps/react-dom.js → pre-bundled ReactDOM
  
  Each file is a separate HTTP request.
  In production, these are bundled. In dev, they're individual.
  This is fast because transforms are per-file (no bundling step).

7. Hot Module Replacement (HMR)

HMR is the ability to update modules in the browser while the application is running, without a full page reload. Vite's HMR is near-instant.

How HMR Works

You edit App.jsx and save:

[1] Filesystem watcher detects change to App.jsx
[2] Vite invalidates App.jsx in its module graph
[3] Vite re-transforms App.jsx (JSX → JS)
[4] Vite sends WebSocket message to browser:
    { type: 'update', path: '/src/App.jsx', timestamp: 1703... }
[5] Browser's HMR client receives the message
[6] Browser fetches the updated /src/App.jsx?t=1703... (cache-busted)
[7] React Fast Refresh:
    - Preserves component STATE (useState values kept!)
    - Re-renders only the changed component
    - Does NOT remount the component (useEffect doesn't re-run)
[8] Updated UI visible in browser

Total time: ~20-100ms
You see the change almost before you look up from your editor.

What HMR Preserves vs Resets

HMR updates (component code changed):

  PRESERVED:
    - useState values (current state kept)
    - useRef values
    - DOM scroll position
    - Input field values
    - Focus state
    
  RE-EXECUTED:
    - Component render function (with current state)
    - useMemo/useCallback (recomputed)
    
  NOT RE-EXECUTED:
    - useEffect (unless dependencies changed)
    - Event handlers (swapped to new version)
    
  FULL RELOAD triggered when:
    - You edit a file that's not a React component
    - You edit a file imported by many modules
    - Syntax error in the file
    - HMR handler throws an error

React Fast Refresh

React Fast Refresh is the React-specific HMR implementation. It's smarter than a generic HMR because it understands React component boundaries:

// You have this component:
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

// User clicks + three times. Count is now 3.

// You edit the component to add color:
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p style={{ color: 'blue' }}>Count: {count}</p>  {/* Added color */}
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

// After HMR:
// Count is STILL 3 (state preserved!)
// But the text is now blue (new rendering applied)
// No page reload. No state loss.

8. Build Process: npm run build

For production, Vite uses Rollup to create optimized bundles:

$ npm run build

# Output:
# vite v5.x.x building for production...
# transforming...
# rendering chunks...
# computing gzip size...
#
# dist/index.html                  0.46 kB │ gzip:  0.30 kB
# dist/assets/index-DiwrgTda.css   1.42 kB │ gzip:  0.74 kB
# dist/assets/index-CdTXrpJf.js  143.36 kB │ gzip: 46.12 kB

What Happens During Build

BUILD PROCESS:

[1] Vite reads all source files
[2] Rollup resolves the dependency graph (starting from index.html)
[3] Transformations:
    - JSX → JavaScript (Babel or SWC)
    - TypeScript → JavaScript (if using TS)
    - CSS Modules → scoped CSS
    - Asset imports → hashed file references
[4] Tree shaking:
    - Remove unused exports
    - Remove dead code
    - Eliminate unused CSS (if configured)
[5] Code splitting:
    - Dynamic imports → separate chunks
    - Vendor code → vendor chunk
    - Shared code → common chunk
[6] Minification:
    - JavaScript minified (esbuild or terser)
    - CSS minified
    - HTML minified
[7] Asset processing:
    - Images optimized (if plugins configured)
    - Fonts inlined or hashed
    - SVGs optimized
[8] Output:
    - dist/index.html (entry point)
    - dist/assets/*.js (JavaScript chunks with content hashes)
    - dist/assets/*.css (CSS with content hashes)
    - dist/assets/* (images, fonts, etc.)

Content Hashing

Vite adds content hashes to filenames for cache busting:

dist/
  index.html
  assets/
    index-CdTXrpJf.js      ← hash changes when content changes
    index-DiwrgTda.css      ← different hash
    logo-BnfkW3kZ.svg       ← image hash
    vendor-X8dj3kfS.js      ← vendor chunk hash

This enables aggressive caching: you can set Cache-Control: max-age=31536000 (1 year) on all assets in /assets/. When code changes, the filename changes, busting the cache automatically. index.html should have Cache-Control: no-cache so the browser always gets the latest entry point.


9. Preview: npm run preview

After building, you can preview the production build locally:

$ npm run preview

# Output:
#   ➜  Local:   http://localhost:4173/
#   ➜  Network: use --host to expose

This serves the dist/ folder using a static file server. It is NOT for production hosting — it's for verifying that the production build works correctly before deploying.

Dev vs Preview vs Production:

  npm run dev:
    - Serves source files directly (unbundled)
    - Transforms on-the-fly
    - HMR enabled
    - Not optimized (no minification, no tree shaking)
    - For development only
    
  npm run preview:
    - Serves the built dist/ folder
    - Same output as production
    - No HMR
    - Optimized and minified
    - For testing the build locally
    
  Production hosting (Vercel, Netlify, etc.):
    - Serves the dist/ folder
    - CDN distribution
    - Proper caching headers
    - HTTPS

10. Vite vs CRA vs Webpack: Comparison

FeatureViteCRA (deprecated)Webpack (manual)
Dev startup~300ms~30-60s~10-30s
HMR speed<100ms2-10s1-5s
Build toolRollupWebpackWebpack
Configvite.config.js (simple)Hidden (eject to access)webpack.config.js (complex)
TypeScriptBuilt-inBuilt-inManual setup
CSS ModulesBuilt-inBuilt-inManual setup
Code splittingAutomatic (dynamic import)ManualManual
Tree shakingRollup (excellent)Webpack (good)Webpack (good)
ProxyBuilt-inBuilt-inManual
SSR supportYesNoManual
Library modeYesNoYes
MaintainedActivelyDeprecatedActively
CommunityGrowing fastDecliningStable

When to Use What

  • Vite: New React projects (recommended default)
  • Next.js: When you need SSR, SSG, or API routes
  • Webpack: Legacy projects or highly custom build requirements
  • CRA: Never for new projects. Migrate existing CRA projects to Vite.

Migrating from CRA to Vite

# High-level migration steps:

# 1. Remove CRA dependencies
npm uninstall react-scripts

# 2. Install Vite
npm install --save-dev vite @vitejs/plugin-react

# 3. Create vite.config.js
# (see configuration section above)

# 4. Move index.html from public/ to project root
# Change <script> tag to type="module" src="/src/index.jsx"

# 5. Update package.json scripts:
#   "dev": "vite",
#   "build": "vite build",
#   "preview": "vite preview"

# 6. Rename environment variables:
#   REACT_APP_* → VITE_*

# 7. Update import.meta.env references:
#   process.env.REACT_APP_API_URL → import.meta.env.VITE_API_URL

# 8. Test everything
npm run dev
npm run build

11. Vite Plugin Ecosystem

Vite's plugin system is compatible with Rollup plugins and has its own plugin API.

Essential Plugins

// @vitejs/plugin-react — Required for React
// Already included in React templates
import react from '@vitejs/plugin-react';

// @vitejs/plugin-react-swc — SWC alternative (faster)
import react from '@vitejs/plugin-react-swc';

Commonly Used Plugins

// vite-plugin-svgr — Import SVGs as React components
import svgr from 'vite-plugin-svgr';

// Usage:
// import { ReactComponent as Logo } from './logo.svg';
// <Logo width={100} height={100} />

// vite-plugin-checker — TypeScript and ESLint checking
import checker from 'vite-plugin-checker';

export default defineConfig({
  plugins: [
    react(),
    svgr(),
    checker({
      typescript: true,
      eslint: {
        lintCommand: 'eslint "./src/**/*.{ts,tsx}"',
      },
    }),
  ],
});
// vite-plugin-pwa — Progressive Web App support
import { VitePWA } from 'vite-plugin-pwa';

// @tanstack/router-vite-plugin — TanStack Router file-based routing
import { TanStackRouterVite } from '@tanstack/router-vite-plugin';

// vite-plugin-compression — Gzip/Brotli compression
import compression from 'vite-plugin-compression';

// vite-plugin-inspect — Inspect intermediate transforms
import inspect from 'vite-plugin-inspect';

Plugin Configuration Example

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import svgr from 'vite-plugin-svgr';
import checker from 'vite-plugin-checker';
import path from 'path';

export default defineConfig({
  plugins: [
    react(),
    svgr({
      svgrOptions: {
        icon: true,
      },
    }),
    checker({
      typescript: true,
    }),
  ],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

12. Environment Variables in Vite

Vite uses .env files for environment-specific configuration, but with a key difference from CRA: variables must be prefixed with VITE_.

.env Files

# .env — loaded in all environments
VITE_APP_TITLE=My React App

# .env.local — loaded in all environments, git-ignored
VITE_API_KEY=your-secret-key-here

# .env.development — loaded only during `npm run dev`
VITE_API_URL=http://localhost:8080/api

# .env.production — loaded only during `npm run build`
VITE_API_URL=https://api.production.com

# .env.development.local — dev + git-ignored
# .env.production.local — prod + git-ignored

Loading Priority

Priority (highest to lowest):

  .env.{mode}.local    (e.g., .env.development.local)
  .env.{mode}          (e.g., .env.development)
  .env.local
  .env

  "mode" is 'development' for `vite` (dev server)
  "mode" is 'production' for `vite build`

Accessing Environment Variables

// In your React code:
const apiUrl = import.meta.env.VITE_API_URL;
const appTitle = import.meta.env.VITE_APP_TITLE;

// Built-in variables:
import.meta.env.MODE          // 'development' or 'production'
import.meta.env.BASE_URL      // Base URL from vite.config.js
import.meta.env.PROD          // true in production
import.meta.env.DEV           // true in development
import.meta.env.SSR           // true during SSR

// IMPORTANT: Only VITE_ prefixed variables are exposed to client code!
// This prevents accidentally leaking server secrets.

// This WILL NOT work in client code:
// DATABASE_URL=postgres://... (no VITE_ prefix → not exposed)
// SECRET_KEY=abc123 (no VITE_ prefix → not exposed)

TypeScript Support for Env Variables

// src/vite-env.d.ts (or env.d.ts)
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_TITLE: string;
  readonly VITE_GOOGLE_ANALYTICS_ID: string;
  // Add more as needed
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

This gives you autocomplete and type checking for environment variables.


13. TypeScript Setup With Vite

If you used the react-ts or react-swc-ts template, TypeScript is pre-configured. Here's what's set up:

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}

Key settings explained:

SettingPurpose
target: "ES2020"Output ES2020 JavaScript (modern browsers)
jsx: "react-jsx"Use the automatic JSX runtime (no import React needed)
strict: trueEnable all strict type checks
moduleResolution: "bundler"Resolve modules like Vite does
noEmit: trueTypeScript only checks types; Vite handles compilation
isolatedModules: trueRequired — each file must be transformable independently

Important: Vite Does NOT Type-Check

Vite's role:        Transform TypeScript → JavaScript (fast, using esbuild)
TypeScript's role:  Type checking (separate step)

Vite strips types but does NOT check them.
A type error will NOT prevent Vite from running.

To get type checking:
  Option 1: IDE (VS Code) — real-time feedback
  Option 2: npm run type-check → "tsc --noEmit"  (CI/CD)
  Option 3: vite-plugin-checker (shows errors in browser overlay)

Add a type-check script to package.json:

{
  "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && vite build",
    "preview": "vite preview",
    "type-check": "tsc --noEmit"
  }
}

The build script now runs TypeScript checking before building. If there are type errors, the build fails.


14. Path Aliases Configuration

Path aliases replace long relative imports with clean, absolute-style imports:

// Without aliases (painful in deep directories):
import Button from '../../../components/ui/Button';
import { useAuth } from '../../../hooks/useAuth';
import { formatDate } from '../../../utils/formatDate';

// With aliases (clean):
import Button from '@/components/ui/Button';
import { useAuth } from '@/hooks/useAuth';
import { formatDate } from '@/utils/formatDate';

Setting Up Path Aliases

Step 1: vite.config.js

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

Step 2: tsconfig.json (if using TypeScript)

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Both files need the alias for it to work: Vite uses vite.config.js to resolve imports at build time, and TypeScript uses tsconfig.json for type checking and IDE autocomplete.


15. Common Vite Configuration Patterns

Pattern 1: API Proxy

During development, your React app runs on port 5173, but your API runs on port 8080. The proxy forwards API requests:

// vite.config.js
export default defineConfig({
  server: {
    proxy: {
      // /api/users → http://localhost:8080/api/users
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
});
// In your React code, just use relative paths:
fetch('/api/users')
  // In dev: proxied to http://localhost:8080/api/users
  // In prod: goes to your actual API server

Pattern 2: Global CSS Variables / SCSS Setup

// vite.config.js
export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        // This SCSS is prepended to every .scss file
        additionalData: `
          @use "@/styles/variables" as *;
          @use "@/styles/mixins" as *;
        `,
      },
    },
  },
});

Pattern 3: Custom Chunk Splitting

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: (id) => {
          // Group all node_modules into a 'vendor' chunk
          if (id.includes('node_modules')) {
            // Further split large libraries into their own chunks
            if (id.includes('react')) return 'react-vendor';
            if (id.includes('@tanstack')) return 'tanstack-vendor';
            return 'vendor';
          }
        },
      },
    },
  },
});

Pattern 4: Multiple Entry Points

// vite.config.js — for apps with multiple HTML pages
export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: 'index.html',
        admin: 'admin.html',
      },
    },
  },
});

16. Key Takeaways

  1. CRA is deprecated. Use Vite. Vite provides 10-100x faster development experience with better defaults and active maintenance.

  2. Vite uses native ES modules in development. Instead of bundling your entire app before serving it, Vite serves individual files on demand. Only imported files are processed.

  3. Vite uses Rollup for production builds. The production output is optimized, minified, tree-shaken, and code-split — ready for deployment.

  4. esbuild makes pre-bundling fast. Dependencies from node_modules are pre-bundled once using esbuild (Go-based, 10-100x faster than JavaScript bundlers).

  5. HMR is near-instant. When you save a file, the change appears in the browser in <100ms with component state preserved, thanks to React Fast Refresh.

  6. Environment variables must use the VITE_ prefix to be exposed to client code. This prevents accidentally leaking server secrets.

  7. Vite does not type-check TypeScript. It strips types for speed. Use tsc --noEmit separately (in build scripts or CI) for type checking.

  8. Path aliases require configuration in two placesvite.config.js for Vite's module resolution and tsconfig.json for TypeScript and IDE support.

  9. The react-swc-ts template is the recommended starting point for production React projects — TypeScript for safety, SWC for speed.

  10. npm run dev for development, npm run build for production, npm run preview to test the build locally. These three commands cover the entire development workflow.


Explain-It Challenge

  1. Explain why Vite's dev server starts in ~300ms regardless of project size, while CRA's dev server takes 30-60 seconds and gets slower as the project grows. What architectural difference causes this?

  2. A teammate is confused: "I added DATABASE_URL to my .env file but import.meta.env.DATABASE_URL is undefined in my React code." Explain why this happens and what the correct approach is.

  3. Describe the complete journey of a single .jsx file from your editor to the browser screen during development with Vite. Include: file save, Vite's processing, browser request, and rendering.


Navigation: ← Real DOM vs Virtual DOM · Next → Project Structure Breakdown