From CRA to Vite: Modernizing React Client Extensions

Introduction
If you have been developing React Client Extensions on Liferay DXP for some time now, it is highly likely that you had a project generated via Create React App. For years, CRA was the standard way of setting up your React apps with sensible defaults and out-of-the-box support for everything you needed. However, if you have generated a new CRA application lately and you saw how it took ages for the dev server to start, you probably guessed that something was wrong.
React community and its tools have evolved in recent years and, for React-based projects, there is nothing else to use except Vite today. After using Vite once, you won't be able to switch back, especially if your app is a bit bigger, like a Liferay Client Extension. In this blog, we will see how you can migrate from CRA to Vite in your React Client Extension project for Liferay DXP 7.4.
What's CRA, and Why is it not recommended?
Create React App was made by the React team for providing developers with an efficient solution to work on React applications without having to manually configure Webpack, Babel, and many more. Back then, it was pretty good.
However, the issue is that it relies on Webpack, which is very powerful but also inefficient – especially when it comes to hot module replacement. It becomes worse and worse as the app scales, and waiting for a few seconds each time you save a file seriously impacts productivity.
Moreover, CRA is not scalable either. There seem to be issues regarding development, because nothing much has been happening at all in the official GitHub repository lately, and even the creators of React advise against using it as a template for a project.
Furthermore, everyone who tried using CRA for setting up their own Webpack configuration knows just how difficult that process is. One may always “eject” from CRA, but in this case, he or she will get too many configuration files instead of having a neatly organized project.
Enter Vite
Vite ("veet" in French, meaning "fast"), a tool designed by Evan You – the creator of Vue.js, but doesn't have any ties with the framework, as it performs great with React.
Technically speaking, Vite operates differently than Webpack during the development process. Instead of the initial compilation, it takes advantage of the built-in native ES module support in browsers. The browser fetches files as needed, and Vite provides extremely fast response times. As a result, cold-start performance is cut down from 20-30 seconds to just 2 in a mid-size project, and all updates will happen instantly. Just edit the file, and see the results on the screen right after.
During production, Vite employs Rollup, a tool with high maintenance and efficient output.
Configuration is also easier now. The configuration file of Vite would be about 20-30 lines long while an ejected CRA Webpack config would include more than 600 lines.
Setting Up for the Migration
It is important to ensure you have Liferay DXP with client extension capabilities running at version 7.4 GA before doing anything else. This migration process is primarily restricted to the frontend. Liferay's build/deployment process doesn’t care what method of building the project you are using, be it CRA or Vite; only where the build artifacts are placed matters.
You should back up your project or work in a different branch from the start. This simple step could save a lot of headaches. There are a couple of small configuration details that could trip things up.
Step 1 : Replace CRA Dependencies with Vite
Open your package.json. You'll have something like this in devDependencies :
1"react-scripts": "5.0.1",Remove it. In its place, add :
1"vite": "^5.0.0",
2"@vitejs/plugin-react": "^4.0.0",To get the new packages, run npm install (or yarn, depending on your configuration). You can also remove any packages like @babel/* or eslint-config-react-app that you may have installed only because CRA needed them and see if you actually need them in your project.
Step 2 : Update Package Scripts
CRA used react-scripts for everything. Replace those script entries :
1"scripts": {
2 "start": "vite",
3 "build": "vite build",
4 "preview": "vite preview"
5}That's it. No wrappers, no hidden layers, just Vite's own CLI commands. The preview script is handy for locally testing your production build before deploying.
Step 3 : Move and Update index.html
And this can trip some people up. In a CRA project, the index.html is placed in the public/ directory with %PUBLIC_URL% tokens. Vite does things differently here. Place index.html in the project root and use it as the bundle entry point instead of serving it as a static file.
Move public/index.html to your project root, then update it. Remove the %PUBLIC_URL% references :
1<!-- CRA style - remove this -->
2<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
3
4<!-- Vite style -->
5<link rel="icon" href="/favicon.ico" />More importantly, You need to add a script tag in the HTML that points directly to your entry file. Vite does not add it automatically like CRA does :
1<body>
2 <div id="root"></div>
3 <script type="module" src="/src/main.jsx"></script>
4</body>The type="module" attribute is required. Vite serves ES modules natively, and the browser needs to know that's what it's loading.
Step 4 : Rename index.js to main.jsx
It is not that strictly necessary, but it is better to follow common practice with Vite projects and make the code easy to read. Renaming src/index.js to src/main.jsx is good as per Vite standards. If you will use JSX or TSX for your root component, naming it .jsx or .tsx will ensure it gets processed properly by Vite.
The content of the file usually doesn't need to change much :
1import React from 'react';
2import ReactDOM from 'react-dom/client';
3import App from './App';
4import './index.css';
5
6
7ReactDOM.createRoot(document.getElementById('root')).render(
8 <React.StrictMode>
9 <App />
10 </React.StrictMode>
11);If you are still using the old ReactDOM.render() API from React 17, it's time to upgrade it to createRoot and also it will be required for React 18.
Step 5 : Create vite.config.js
Add a vite.config.js file at your project root. Here's a solid baseline for a Liferay Client Extension :
1import { defineConfig } from 'vite';
2import react from '@vitejs/plugin-react';
3
4
5export default defineConfig({
6 plugins: [react()],
7 build: {
8 outDir: 'build',
9 rollupOptions: {
10 output: {
11 entryFileNames: 'static/js/[name].js',
12 chunkFileNames: 'static/js/[name].js',
13 assetFileNames: 'static/[ext]/[name].[ext]',
14 },
15 },
16 },
17 server: {
18 port: 3000,
19 open: true,
20 },
21});First of all, there are a few things you'll want to keep in mind. The outDir: 'build' property will cause Vite to write files into a folder called build/, which follows the CRA convention. If you've defined a specific output directory for your client-extension.yaml file, then this needs to be consistent.
The rollupOptions configuration block determines how the names of output files will be generated. The file naming conventions used by Liferay's asset pipeline may be pretty strict, so having a predictable filename can go a long way here.
Step 6 : Handle Environment Variables
CRA used REACT_APP_ as a prefix for environment variables. Vite uses VITE_ instead. Any .env file entries you have will need updating :
1# Before (CRA)
2REACT_APP_API_BASE_URL=https://your-liferay-instance.com
3
4# After (Vite)
5VITE_API_BASE_URL=https://your-liferay-instance.comIn your code, swap process.env.REACT_APP_* references to import.meta.env.VITE_* :
1// Before
2const apiUrl = process.env.REACT_APP_API_BASE_URL;
3
4// After
5const apiUrl = import.meta.env.VITE_API_BASE_URL;It's a mechanical find-and-replace, but don't skip it, your app will silently fail if it's reading undefined environment variables.
Step 7 : Update client-extension.yaml
Your client-extension.yaml probably references the build output for deployment. Make sure the url or sourceCodeURL fields and any path references still point to the right locations after your Vite config changes.
A typical React Client Extension entry looks something like this :
1your-react-app:
2 name: Your React App
3 type: customElement
4 htmlElementName: your-react-app
5 urls:
6 - /o/your-react-app/index.html
7 sourceCodeURL: https://github.com/your-org/your-repoThe important part here is to ensure that all the urls lead correctly to where your index.html has been packaged after the Liferay packaging process. Make sure that none of the URLs point to an old location if you have customized the outDir parameter.
Step 8 : Build and Deploy
Once everything is in place, run npm run build. Vite will compile your app into the build/ directory (or wherever you pointed outDir).
From there, the Liferay deployment process is the same as it was with CRA. If you're using the blade CLI or deploying via a workspace task, point it at your output directory and deploy as usual. Liferay doesn't care what bundler produced the files, it just needs the compiled assets.
For local development, npm run start will launch Vite's dev server. First load is dramatically faster than what you're used to with CRA. HMR updates are nearly instant. The difference is noticeable enough that your team will probably comment on it within the first day.
Common Issues and How to Fix Them
JSX in .js files breaks. Vite will not automatically process JSX in files with a .js extension the way CRA did. You have to rename your files to .jsx, or add this to below to your vite.config.js :
1esbuild: {
2 include: /src\/.*\.[jt]sx?$/,
3 exclude: [],
4}Absolute imports stop working. CRA supported src/ as a root for absolute imports. To replicate this in Vite :
1resolve: {
2 alias: {
3 '@': '/src',
4 },
5},Then update your imports to use @/components/... instead of bare components/....
process.env is undefined. If you have third-party libraries that reference process.env directly (common with older Node-style packages), you'll need to add a define shim :
1define: {
2 'process.env': process.env,
3},CSS Modules behave slightly differently. CSS Modules work straight out of the box with Vite, but the default behavior when it comes to generating class names is different from CRA. If your tests depend on generated class names, then you may have to tweak the configuration settings for cssModules in Vite.
Wrapping Up
Migrating a React Client Extension from CRA to Vite isn't a massive undertaking. For a typical project, most of this takes an afternoon. The steps are straightforward: swap dependencies, update scripts, move and update index.html, create a vite.config.js, fix environment variables, and verify your deployment paths still line up.
You get faster development cycles not only in terms of initial setup but also whenever you make changes along the way. The config becomes more transparent, and the build chain will be actively maintained by an enthusiastic community. Moreover, as Vite is the direction the Node.js community is moving towards, there is no need to struggle with abandoned technology stacks.
Talking specifically about Liferay Frontend Development, choosing CRA now seems like looking at the past. With Liferay Client Extensions becoming more mature every day and the number of projects built using this technology increasing, it makes sense to choose something else.
In case of any problems related to your configuration that are not discussed above, the official documentation provided by Vite is well-written and more comprehensible than Webpack's. The Discord and GitHub communities are also very lively, which makes it a good idea to consult them first.
Good luck with the migration. Your future self, who is not waiting 40 seconds for the dev server to start, will thank you.