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", mostly due to the project's stagnation.
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:
CRA | Vite | |
---|---|---|
version | 4.0.3 | 2.2.3 |
number of dependencies | 3681 | 76 |
size of dependencies | 154 MB | 38 MB |
rm -rf node_modules && yarn | 86s | 5s |
start dev server | 8.3s | ~1s |
This comparison is anything but fair. CRA provides so much out of the box: Jest, ESLint, PostCSS, Babel plugins, polyfills and much more. So here are some numbers from one of my projects that I've migrated:
CRA | Vite | |
---|---|---|
number of dependencies | 3288 | 1164 |
size of dependencies | 326 MB | 191 MB |
rm -rf node_modules && yarn | 4m56s | 1m30s |
start dev server (1st run) | 1m14s | 4s |
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:
npm ls --parseable | wc -l
# number of dependencies (deduped)
time (rm -rf node_modules && yarn)
# execution time for reinstalling node_modules
Last but not least, Vite does not only support React, but also Preact, Vue, Svelte and Lit out of the box.
Migration
The required steps to convert an existing CRA project to Vite depend on how complex your app is. In this section I will describe all the changes I've applied to migrate my own projects.
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": "1.1.1",
+ "vite": "2.7.0
+ }
- Run
npm install
oryarn
. I recommend deletingnode_modules
beforehand. - Replace scripts in
package.json
(I'll covertest
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
toindex.html
(project root folder). - Remove
%PUBLIC_URL%
fromindex.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 react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
}
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. There's also vite-plugin-env-compatible, but I haven't tried it out.
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}"`
},
This method does not support mode-specific .env
files like .env.development
, so you'll have to set this up yourself if required.
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 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.
- CRA uses
- 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: [react()]
+ plugins: [react(), 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.jsontypes
. The main drawback is that you will have to list all yourtypes
explicitly if you're currently relying ontypeRoots
.
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.
Storybook
In one of my projects, I am using Storybook as my design system tool. Storybook uses Webpack by default and integrates nicely with CRA via a plugin. After migrating the main application to Vite, the Storybook setup was broken. There is an experimental Vite builder, but I couldn't make it work. For now, I'll keep using Webpack for Storybook while the application itself runs on Vite. This required some additional Webpack configuration (loaders, alias resolvers).
To keep Webpack dependencies away from my Vite project, I've moved Storybook into a separate module.
Conclusion
I've migrated five Create React App projects to Vite, e.g. dashboard, static-marks and board-games (check out those links to see the exact changes I've made). My experience was mostly pleasant. At first, I failed to migrate my biggest project. With Vite 2.7, most of the previous issues disappeared, though.
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.
As of December 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. However, migrating away from Create React App was definitely worth it.
As always, review the trade-offs and pick the right tool for the job.