How to Migrate from Create React App to Vite
·PkgPulse Team
TL;DR
CRA to Vite migration takes 30-60 minutes and gives you a 40x faster dev server. Create React App was deprecated in 2023 and should not be used for new projects or maintained indefinitely. The migration is well-documented and mechanical: remove react-scripts, install Vite, move index.html, rename env vars from REACT_APP_ to VITE_, update imports from process.env to import.meta.env. Most apps migrate with under 20 file changes.
Key Takeaways
- Dev server: 8,000ms → 200ms — CRA bundles everything; Vite serves ESM natively
- 5 main changes:
react-scripts→vite, index.html location, env vars, tsconfig, scripts - Tests: CRA used Jest; switch to Vitest (see Jest → Vitest guide) or keep Jest standalone
- CRA was deprecated 2023 — no security patches on CRA vulnerabilities
- TypeScript CRA: same migration, just add TypeScript-specific steps
The Full Migration
1. Remove react-scripts
# Remove CRA
npm uninstall react-scripts
# Verify CRA is gone
cat package.json | grep react-scripts # Should show nothing
2. Install Vite
npm install -D vite @vitejs/plugin-react
# For TypeScript CRA:
npm install -D vite @vitejs/plugin-react typescript
# Common extras:
npm install -D vite-tsconfig-paths # Reads paths from tsconfig
npm install -D vite-plugin-svgr # SVG as React component (if you use it)
3. Create vite.config.ts
// vite.config.ts — at project root
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import svgr from 'vite-plugin-svgr'; // Only if you import SVGs
export default defineConfig({
plugins: [
react(),
tsconfigPaths(), // Handles paths from tsconfig.json
svgr(), // If you use: import { ReactComponent as Logo } from './logo.svg'
],
server: {
port: 3000, // Match CRA's default port
open: true, // Auto-open browser (like CRA)
},
build: {
outDir: 'build', // CRA used 'build'; Vite default is 'dist'
},
});
4. Move index.html to Root
# CRA: public/index.html
# Vite: index.html at project root
mv public/index.html ./index.html
<!-- index.html — edit after moving -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- CRA: %PUBLIC_URL%/favicon.ico — replace with /favicon.ico -->
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!-- ADD THIS LINE — Vite requires explicit entry point -->
<script type="module" src="/src/index.tsx"></script>
<!-- ↑ .tsx for TypeScript, .jsx for JavaScript -->
</body>
</html>
5. Update package.json Scripts
// Before (CRA):
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}
// After (Vite):
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest run"
}
}
6. Rename Environment Variables
# CRA: REACT_APP_ prefix → Vite: VITE_ prefix
# Edit your .env files:
# Before (.env, .env.local, .env.production):
REACT_APP_API_URL=https://api.example.com
REACT_APP_GOOGLE_MAPS_KEY=abc123
# After:
VITE_API_URL=https://api.example.com
VITE_GOOGLE_MAPS_KEY=abc123
// Update all usage in your code:
// Before:
const apiUrl = process.env.REACT_APP_API_URL;
if (process.env.NODE_ENV === 'development') { ... }
// After:
const apiUrl = import.meta.env.VITE_API_URL;
if (import.meta.env.DEV) { ... } // Boolean, not string
if (import.meta.env.PROD) { ... }
7. Update TypeScript Config
// tsconfig.json — update for Vite
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler", // ← Key change for Vite
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true, // Vite handles emit
"jsx": "react-jsx", // Same as CRA
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"types": ["vite/client"] // Add Vite's types (import.meta.env)
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
// tsconfig.node.json (for vite.config.ts itself)
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
CRA-Specific Gotchas
SVG Imports
// CRA: SVG as React component (auto-configured)
import { ReactComponent as Logo } from './logo.svg';
// Vite: requires vite-plugin-svgr
import { ReactComponent as Logo } from './logo.svg?react';
// Note the ?react query parameter
// Or update import syntax with svgr plugin configured:
import Logo from './logo.svg?react'; // Default export as component
Public Folder References
<!-- CRA: use %PUBLIC_URL% in index.html -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!-- Vite: use root-relative paths (no variable needed) -->
<link rel="manifest" href="/manifest.json" />
window.__env__ / Dynamic Runtime Config
// CRA apps sometimes inject env at runtime via public/env-config.js
// Vite bakes env at build time — same as CRA's default behavior
// If you need runtime config, use a server endpoint or meta tags
Verify Migration Success
# Start dev server
npm run dev
# ✅ Should start in <500ms at localhost:3000
# Build for production
npm run build
# ✅ Should complete, output in /build
# Preview production build
npm run preview
# ✅ Should serve from /build
# Check for CRA remnants
grep -r "react-scripts\|REACT_APP_\|process\.env\.NODE_ENV" src/
# ✅ Should find nothing after migration
Compare Vite and Webpack package health on PkgPulse.
See the live comparison
View vite vs. webpack on PkgPulse →