It's #FrontendFriday

It's #FrontendFriday - React Scripts to Vite Migration

Hello #FrontendFriday-reader, this week in the Frontend Friday I present a technical deep dive of a migration that I recently carried out in one of our projects.

Initial situation

The initial situation is a React frontend that is still based on react-scripts sets. In order to modernize this and bring all dependencies up to date (especially TypeScript), I have changed the tooling to Vite migrated.

Why Vite?

Vite is advertised as a modern tooling variant that is quick and easy and is intended to improve the developer experience. In particular, Vite offers the following advantages out-of-the-box:

  • Faster dev server start times through native ES modules
  • Hot Module Replacement (HMR)
  • Simplified configuration

Install Vite and uninstall react-scripts

In this Deep Dive I use npm as a package manager, so I use the corresponding npm commands.

First, I install Vite and the required React plugin:

npm install --save-dev vite @vitejs/plugin-react

Then I remove react-scripts from the dependencies:

npm uninstall react-scripts

Update package.json

In my package.json I can now simply use the react-scripts-commands with the corresponding Vite commands, for example:

Before:

"scripts": { 
"start": "react-scripts start", 
"build": "react-scripts build" 
}

After:

"scripts": { 
"start": "vite", 
"build": "vite build" 
}

Customize environment variables

There was already a first difficulty here in my project. For local development, I use environment variables that were passed to the process via my start script (via env-cmd from a .env-file):

"start": "docker-compose -f localdeployment/docker-compose.yml up -d && env-cmd -f .env.local react-scripts start"

Vite replaces environment variables statically, and I can no longer use process.env to access them. Instead, I had to adjust the variables in the individual files of the respective stage and change them as follows:

REACT_APP_BASE_IMAGE_URL=/

becomes

VITE_BASE_IMAGE_URL=/

With the prefix VITE_ Vite recognizes the respective variable. After the change, I now have access to it via import.meta.env.VITE_BASE_IMAGE_URL and can adjust the corresponding position in the code.

The previously used start script changes to :

"start": "docker-compose -f localdeployment/docker-compose.yml up -d && vite"

Create the Vite configuration

// vite.config.js 
import { defineConfig } from 'vite'; 
import react from '@vitejs/plugin-react'; 
import svgr from 'vite-plugin-svgr'; 
import eslintPlugin from 'vite-plugin-eslint'; 

export default defineConfig(({ mode }) => { 
let base = ''; 

if (mode !== 'development') { 
base = '/test/basepath'; 
} 

return { 
plugins: [react(), eslintPlugin(), svgr()], 
base, 
build: { 
outDir: 'build', 
sourcemap: false, 
assetsDir: '', 
cssCodeSplit: true, 
rollupOptions: { output: { 
entryFileNames: 'static/js/main.[hash].js', 
chunkFileNames: (assetInfo) => { 
if (assetInfo.name === 'index') { 
return 'static/js/main.[hash].js'; 
} 
return 'static/js/[name].[hash].chunk.js'; 
}, 
assetFileNames: (assetInfo) => { 
if (assetInfo.name && assetInfo.name.endsWith('.css')) { 
return 'static/css/main.[hash].css'; 
} else if ( 
assetInfo.name && 
/\.(png|jpe?g|gif|svg|webp|ico|avif)$/.test(assetInfo.name) 
) { 
return 'static/media/[name].[hash][extname]'; 
} else if ( 
assetInfo.name && 
/\.(woff2?|eot|ttf|otf)$/.test(assetInfo.name) 
) { 
return 'static/media/[name].[hash][extname]'; 
} else { 
return 'static/media/[name].[hash][extname]'; 
} 
}, 
}, 
}, 
}, 
server: { 
port: 3000, 
}, 
}; 
});

To get the same behavior as before, I had to configure a few things.

The most important point is the React pluginso that Vite knows that I have a React app. Also, Vite doesn't have built-in ESLint support; I had to add it via a plugin:

npm install --save-dev vite-plugin-eslint

I also use many SVGs as React components in my application, which is not natively supported by Vite. For this I use the svgr-plugin:

npm install --save-dev vite-plugin-svgr

One time-consuming point was to adapt the build so that I didn't have to make any major changes in my pipeline and when integrating the front end compared to react-scripts had to be made. I therefore continued to use the structure of the previous build and configured it accordingly in Vite. For example, the bundling of the assets, their naming and the folder structure had to be adapted in order to deliver the build correctly.

Import of SVGs as React components

The SVG was originally imported as follows:

import { ReactComponent as IconClose } from '../assets/icons/shared/close.svg';

However, after switching to Vite, I received the error:

TS2614: Module "*.svg" has no exported member 'ReactComponent'. Did you mean to use 'import ReactComponent from "*.svg"' instead?

By using the svgr-plugins, I can now import the SVG as follows:

import IconClose from '../../assets/icons/shared/close.svg?react';

I have now completed everything on the build side and the application can be started locally and delivered in the pipeline.

Testing

react-scripts was also used to start my tests. A first naive approach was to start the existing tests directly with Jest to be executed, as react-scripts also relies on Jest under the hood. After many error messages and adjustments to configurations without much success, I switched to Vitest set. Although some error messages also occurred here and configuration was required, I found the setup easier.

In the end I have the following vitest.config.js created:

import { defineConfig } from 'vite'; 
import react from '@vitejs/plugin-react'; 
import svgr from 'vite-plugin-svgr'; 

export default defineConfig({ 
plugins: [react(), svgr()], 
test: { 
environment: 'jsdom', 
globals: true, 
setupFiles: 'src/setupTests.ts', 
}, 
});

Important points here were the import of svgrso that the SVG components can be imported correctly in the tests. I also use global functions and values that need to be mocked. Therefore I have globals on true and configured a test setup.

In addition, I had to change every place in the code where Jest functions are accessed to vi change. For example:

Before:

setClickable = jest.fn().mockImplementation((flag: boolean): void => { // ... });

After:

setClickable = vi.fn().mockImplementation((flag: boolean): void => { // ... });

After I had made these changes, there was another error with SVGs from a library. These were imported by the tests as React components, but were not exported as such in the library itself. Here I had to create a mock, which was not necessary before, to avoid errors in the tests.

Conclusion:

The changeover to Vite involved a few obstacles, but I was able to solve them in a reasonable amount of time. In total, the migration took about two days. The biggest challenge was integrating the whole thing into an existing project.

With regard to the advantages or disadvantages compared to react-scripts I still lack experience from the project. So far, however, the change has been at least positive-neutral, has had no unplanned consequences and I am now using a modern build tool.

 

Alexander Wahl

About ME

Alexander Wahl has been working as a Frontend Developer at doubleSlash in Friedrichshafen on Lake Constance since 2022. He completed his studies in Munich and Weingarten, where he already focused on web technologies and applications. Since then, he has been working with 3D development as well as React and Angular.

All contributions from Alexander Wahl

Learn more

Further information on our website and in our newsletter

Arrow up