Migrating from CRA to Vite

Why Migrate?
Create React App ( CRA ) has been a popular choice for any one to start with a React applications for years. However, on February 14, 2025 , Facebook announced sunsetting CRA, which has led to a surge in interest in alternative build tools. One another reason is CRA does not provide support for Server Side Rendering ( SSR ) out of box which made applications perform worst on SEO. So to overcome this we need to run npm run eject and maintain a custom setup with webpack and add lot of plugin on top of that. To avoid this, one of the most popular alternatives is Vite, a modern build tool that offers faster development and improved performance.
Why Vite?
- Out of box support to React and SSR
- Exclusive plugin ecosystem
- Minimal code changes required for migration
- Community support
- Easy to configure and extend
Our end goal was to setup a custom SSR solution, so Vite was our go to choice.
For this post I have taken a sample project which was created using CRA and migrated it to Vite.
CRA version: https://github.com/igowthaman/games-world/tree/cra
Vite version: https://github.com/igowthaman/games-world
Here the list of steps I followed
- Install vite and vite react plugin
npm install --save-dev vite @vitejs/plugin-react - Add vite.config.js to base dirJavaScript
import { defineConfig, transformWithOxc, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; const transformJsxInJs = () => ({ name: "transform-jsx-in-js", enforce: "pre", async transform(code, id) { if (!id.match(/.*.js$/)) { return null; } return await transformWithOxc(code, id, { lang: "jsx", }); }, }); export default defineConfig((mode) => { const env = loadEnv(mode, process.cwd(), "REACT_APP_"); const envKeys = {}; Object.entries(env).forEach(([key, value]) => { envKeys[`process.env.${ key }`] = JSON.stringify(value); }); return { plugins: [react(), transformJsxInJs()], define: envKeys, optimizeDeps: { rolldownOptions: { loader: { ".js": "jsx", }, }, }, build: { outDir: "build", }, server: { port: 3000, open: true, }, }; }); - Move the index.html file from public dir to base dir
- change %PUBLIC_URL% to /
<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> - <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> + <link rel="icon" href="/favicon.ico" /> <title>Games World</title> </head> - Add the main script tag to the index.html file
<body> <noscript>You need to enable JavaScript to run this app.</noscript> - <div id="root"></div> + <div id="root"></div> + <script type="module" src="/src/index.js"></script> </body>
- Update the script in package.json
"scripts": { - "start": "react-scripts start", + "start": "vite dev", - "build": "react-scripts build", + "build": "vite build", "test": "react-scripts test", "eject": "react-scripts eject" },
Challenges Faced
Vite does not support jsx in .js files by default, which meant I had to either rename my files to .jsx or configure Vite to handle JSX in .js files. I chose the latter option and added a custom plugin to my Vite configuration to enable JSX support in .js files.
JavaScriptconst transformJsxInJs = () => ({ name: "transform-jsx-in-js", enforce: "pre", async transform(code, id) { if (!id.match(/.*.js$/)) { return null; } return await transformWithOxc(code, id, { lang: "jsx", }); }, });In CRA, environment variables are prefixed with REACT_APP_ and accessed via process.env. In Vite, environment variables are prefixed with VITE_ and accessed via import.meta.env. We can change the env prefix by
envPrefixconfiguration in vite.config.js file.And using define option we can make sure that our existing code which is using process.env to access env variables will work without any change.// CRA const apiUrl = process.env.REACT_APP_API_URL; // Vite const apiUrl = import.meta.env.VITE_API_URL;JavaScriptexport default defineConfig((mode) => { const env = loadEnv(mode, process.cwd(), "REACT_APP_"); const envKeys = {}; Object.entries(env).forEach(([key, value]) => { envKeys[`process.env.${ key }`] = JSON.stringify(value); }); return { plugins: [react()], define: envKeys, envPrefix: "REACT_APP_", }; });Vite uses ES modules, which meant I had to update my import and export statements to be compatible with ES module syntax. This was a bit time-consuming, but it ultimately improved the maintainability of my codebase.
global will not work in Vite as it does in CRA. We need a polyfill for that. I added the following code to my main entry file to polyfill global.
window.global = window
Conclusion
Overall, migrating from CRA to Vite was a positive experience that provided me with a better development workflow and improved performance. If you're considering making the switch, I highly recommend giving Vite a try!