Darek Kay's picture Darek Kay

Migrating a Create React App project to Vite

Create React App (CRA) provides an all-in-one development toolchain for your React applications. It is great for beginners, as you don't need to care about configuring your toolset. However, I've encountered more and more limitations without "ejecting".

Vite is the new hot thing in the frontend world. It leverages new advancements in the ecosystem to provide a faster and leaner development experience. In this post I'll go through my motivation and experience of migrating my Create React App projects to Vite.

Table of contents:

What's the hype about?

At its core, Vite is a pre-configured, extendable wrapper for two JavaScript module bundlers:

Esbuild provides instantaneous builds using ES modules, but it's officially not yet production-ready. That's why rollup, a slower but more mature tool creates the production bundle. This approach provides a great developer experience combined with a stable, optimized production build. One drawback is a potential inconsistency between both tools. However, Vite considers removing rollup as soon as esbuild becomes stable enough.

So how fast is Vite really? Here's my experience for the default starter projects:

CRAVite
version4.0.32.2.3
number of dependencies368176
size of dependencies154 MB38 MB
rm -rf node_modules && yarn86s5s
start dev server8.3s~1s

This comparison is anything but fair. CRA provides so much out of the box: Jest, ESlint, Polyfills, PostCSS, Babel plugins and much more. So here are some numbers from one of my projects that I've migrated:

CRAVite
number of dependencies32881164
size of dependencies326 MB191 MB
rm -rf node_modules && yarn4m56s1m30s
start dev server (1st run)1m14s4s
start dev server (2nd run)9.4s~1s

Please notice that the times are not representative, as I ran my tests on Windows/HDD. What matters are the relative differences, though, and Vite is a clear winner in every single metric. As promised, the dev server starts instantaneously.

Bonus: here's how I got the stats:

# number of dependencies (deduped)
$ npm ls --parseable | wc -l

# execution time for reinstalling node_modules
$ time (rm -rf node_modules && yarn)

Last but not least, Vite does not only support React, but also Preact, Vue, Svelte and Lit out of the box.

Migration

Mandatory steps

First, let's make the application run with Vite:

  • Replace dependencies in package.json:
   "dependencies": {
- "react-scripts": "4.0.3"
},
+ "devDependencies": {
+ "@vitejs/plugin-react-refresh": "1.3.3",
+ "vite": "2.3.7"
+ }
  • Run npm install or yarn. I recommend deleting node_modules beforehand.
  • Replace scripts in package.json (I'll cover test later):
   "scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
- "eject": "react-scripts eject"
+ "start": "vite",
+ "build": "vite build"
}
  • Rename files containing JSX from *.js to *.jsx.
  • Move public/index.html to index.html (project root folder).
  • Remove %PUBLIC_URL% from index.html:
-  <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+ <link rel="icon" href="/favicon.ico" />
- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+ <link rel="apple-touch-icon" href="/logo192.png" />
- <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+ <link rel="manifest" href="/manifest.json" />
  • Add entry point in index.html:
   <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
+ <script type="module" src="/src/index.jsx"></script>
  • Add a vite.config.js file:
import { defineConfig } from "vite";
import reactRefresh from "@vitejs/plugin-react-refresh";

export default defineConfig({
plugins: [reactRefresh()],
}

If your project is small enough, chances are you're done! Read on for further steps that might be required.

Environmental variables

CRA doesn't use any custom configuration file. Instead, most settings are available using environmental variables, e.g. via an .env file or within npm scripts using cross-env. In this section I'll show how to migrate those settings into Vite. If you're not using any env variables in your project, you can skip this section.

Let's assume this is your .env file:

PORT=8001
PUBLIC_URL=/awesome-project/
REACT_APP_NAME=My App
REACT_APP_VERSION=$npm_package_version

First, some of those settings belong into vite.config.js:

-  PORT=8001
- PUBLIC_URL=/awesome-project/
export default defineConfig({
base: "/awesome-project/",
server: {
port: 8001,
}, ...
]);

Next, let's see how to handle custom application variables (starting with REACT_APP_). Here's the Vite way™:

# Define custom env variable (.env file)
- REACT_APP_NAME=My App
+ VITE_NAME=My App

# Access custom env variable (e.g. React component)
- process.env.REACT_APP_NAME
+ import.meta.env.VITE_NAME

This will work perfectly fine within the application, but import.meta will cause problems in Jest. With Babel, you could use babel-preset-vite to fix it. Another alternative is to use dotenv manually to load your custom env variables and pass them down to your application using Vite's define:

import dotenv from "dotenv";
dotenv.config();

export default defineConfig({
define: {
"process.env.VITE_NAME": `"${process.env.VITE_NAME}"`
},
Note: it's important to wrap "define" values in quotes.

One special case is the process.env.NODE_ENV variable, often used to differentiate between development and production builds. Here's one way to access the mode:

export default (mode) => {
return defineConfig({
define: {
"process.env.NODE_ENV": `"${mode}"`,
},
});
};

Finally, CRA supports expanding variables, which Vite does not (at least not from process.env). If you are using variables like $npm_package_version in your .env file, you need to inject them manually using one of the methods above.

Absolute imports

Your project might depend on absolute imports:

import Button from "common/button"; // <root>/src/common/button.jsx

CRA uses webpack aliases to resolve such imports. Vite supports a similar concept:

export default defineConfig({
resolve: {
alias: [{
find: "common", replacement: resolve(__dirname, "src/common")
}]
},
...

For TypeScript projects, check out vite-tsconfig-paths instead. This plugin will automatically pick up your paths from your tsconfig.json file.

Optional steps

Some additional steps may be required, depending on your project setup:

  • If you are using React 17+ and omit the React import for JSX, you will either need to add those imports or let Vite inject them or use vite-react-jsx.
  • If you are using a CSS pre-processor, install the corresponding dependency, e.g. yarn add -D sass for SASS.
  • The default output folder is different. Either change the Vite setting (build.outDir) or adjust your CI/CD pipeline:
    • CRA uses build folder by default.
    • Vite uses dist folder by default.
  • Install and configure optional plugins in vite.config.js, e.g. vite-plugin-legacy to support older browsers or vite-plugin-svgr to import SVG files as React components:
+  import svgr from "vite-plugin-svgr";

- plugins: [reactRefresh()]
+ plugins: [reactRefresh(), svgr()]
  • For TypeScript projects, create an src/vite-env.d.ts file with the following content:
/// <reference types="vite/client" />
  • Alternatively, you can add vite/client to your tsconfig.json types. The main drawback is that you will have to list all your types explicitly if you're currently relying on typeRoots.

Linter and test runner

Making ESlint and Jest work without react-scripts is more tricky, because CRA hides the whole setup. If you want to keep your current configuration, I recommend using the eject script as the first step of the migration. This command will create all the necessary configuration files and scripts required to run ESlint and Jest. You can then manually delete everything you don't need.

I've already extracted all my development tooling into a separate package, so the transition was rather straightforward. But that's a story for another blog post.

Conclusion

I've migrated four Create React App projects to Vite, e.g. static-marks and board-games. My experience was mostly pleasant. However, I failed to migrate my biggest project. After solving most of the issues, I came to a dead end with a few 3rd-party dependencies. I'll need to investigate more here.

I think the frontend tools we've been using are being slowly overtaken by the next generation tooling. Esbuild is a great example of how fast your local development setup can run. Vite takes away the complexity of configuration and lets you create applications in seconds.

However, as of June 2021, Vite is still rough around the edges. It's possible you will encounter many issues trying to migrate your project from another stack. There's also no out-of-the-box ESlint or Jest setup.

As always, review the trade-offs and pick the right tool for the job.


Related posts

Want to leave a comment?

Join the discussion at Twitter or Mastodon. Feel free to drop me an email. 💌

Migrating a Create React App project to Vite