You're shipping on a dead toolchain. The CRA → Vite escape guide for this weekend.
CRA (Create React App) is officially deprecated and teams stuck on it are searching for a fast, low-risk path to Vite. Here's the complete migration in order — env vars, config, SVGs, aliases, and the four gotchas nobody warns you about.
I migrated three CRA projects to Vite last month. The first one took a Saturday afternoon. The second took two hours. The third took forty minutes. The pattern is the same every time — a handful of specific changes in a specific order, and then it's done. This post is that order.
CRA was officially deprecated in March 2023. The React team removed it from the docs in 2024. In 2026, if you're still running react-scripts, you're running unpatched webpack under a dead project. Every npm audit is a reminder. React 19's install warnings make it worse. The exit is less work than it looks.
Why Vite is the right replacement
Vite's dev server doesn't bundle. It serves native ESM and lets the browser resolve imports. Cold start goes from 15–30 seconds on a large CRA project to under a second. HMR updates in milliseconds instead of seconds. Production builds use esbuild (fast) + Rollup (correct tree-shaking). The ecosystem matches: every React framework — Remix, Next, TanStack Start — either uses Vite or is compatible with it.
Step 1 — Install Vite and the React plugin
# Install Vite and the React plugin
npm install --save-dev vite @vitejs/plugin-react
Step 2 — Create vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import svgr from 'vite-plugin-svgr' // only if you import SVGs as components
import path from 'path'
export default defineConfig({
plugins: [
react(),
svgr(), // remove if you have no SVG component imports
],
resolve: {
alias: {
// mirrors tsconfig "paths" — both must stay in sync
'@': path.resolve(__dirname, './src'),
},
},
})
If you don't use SVG imports as React components, skip vite-plugin-svgr entirely. If you don't use @/ absolute imports, skip the resolve.alias block. Start minimal and add only what breaks.
Step 3 — Move index.html to the project root
CRA owns public/index.html and injects scripts automatically. Vite doesn't — you own the file and you wire it up yourself. Move it from public/ to the project root, then add one line at the bottom of :
<!-- index.html at project root, not public/ -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
</head>
<body>
<div id="root"></div>
<!-- This line is new — Vite needs it to find your entry point -->
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Also remove every %PUBLIC_URL% reference. CRA replaced that at build time; Vite does not. Replace with / or a relative path — Vite serves from the project root by default.
Step 4 — Update package.json scripts
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
}
}
Note the separate tsc in the build script. CRA ran type-checking silently in the background and never blocked the build. Vite doesn't type-check — esbuild strips types, it doesn't verify them. Running tsc first means a type error actually fails the build. This is the correct behaviour.
Step 5 — Rename env variables
This is the most tedious step but it's mechanical. CRA uses REACT_APP_ as the required prefix. Vite uses VITE_. Every variable in every .env file needs renaming, and every usage in source needs updating.
# Find every env var reference in source
grep -r "REACT_APP_" src/
grep -r "process.env" src/
// Before — CRA
const apiUrl = process.env.REACT_APP_API_URL
// After — Vite
const apiUrl = import.meta.env.VITE_API_URL
// Vite also gives you these for free, no prefix needed:
// import.meta.env.MODE → 'development' | 'production'
// import.meta.env.DEV → true in dev
// import.meta.env.PROD → true in production
Step 6 — Update tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["vite/client"],
"paths": { "@/*": ["./src/*"] },
"baseUrl": ".",
"strict": true,
"jsx": "react-jsx"
}
}
The critical changes: module from commonjs to ESNext, moduleResolution to bundler (new in TS 5, matches how Vite resolves), and types: ["vite/client"] which gives you proper types for import.meta.env. Without that last one, TypeScript will complain about import.meta.
The four gotchas that will bite you
require() calls in your source files. Vite outputs native ESM — require() doesn't exist. Search for it globally and replace with import statements. If you genuinely need require for a Node interop case, use createRequire from 'module'.
process.env references you missed. import.meta.env only exposes VITE_* variables. Anything else — process.env.NODE_ENV, process.env.npm_package_version — is undefined at runtime. Run grep -r "process.env" src/ after your initial pass and fix every remaining hit.
SVG component imports. CRA's webpack config handled import { ReactComponent as Logo } from './logo.svg' via a built-in loader. Vite doesn't. Install vite-plugin-svgr and change the import to import Logo from './logo.svg?react'. The query string ?react is Vite's way of routing the import through the SVGR transform.
Jest tests. Vite is not a Jest runner. If you're using react-scripts test, your test setup is CRA's Jest config. Don't fix this on the same day as the migration — get the app running first. Then either switch to Vitest (same config file as Vite, near-identical API) or keep Jest by installing babel-jest and a Babel config.
Verify the migration
# These three commands should all succeed cleanly
npm run dev # → app opens at localhost:5173, HMR works
npm run build # → dist/ produced with no type errors
npm run preview # → serves built dist/ locally at localhost:4173
If npm run dev opens your app and hot reload works, and npm run build produces a dist/ folder without errors — you're done. The migration is complete. The three commands above are your acceptance criteria.
Step 7 — Remove react-scripts
Once npm run build succeeds with no errors, react-scripts has done its job. Remove it:
npm uninstall react-scripts
What I want you to notice
The migration is not one big refactor. It's six small, independent changes that each target one CRA assumption. You can do them in order and test after each step. The sandbox below walks through every change in code — run it before you start so you have a reference for what maps where.
CRA → Vite Migration Map — interactive reference
CRA served its purpose for years. It lowered the barrier to starting a React project when the tooling landscape was genuinely confusing. That era is over — Vite is faster, smaller, and the default for everything new. The migration is a Saturday morning. Do it before it becomes someone else's emergency.
Top comments (0)