Migrating React Client Extensions to vite from Deprecated CRA

Learn how to migrate from CRA to Vite while working with React Client Extensions in Liferay DXP 7.4

Best practices to follow while Migrating React Client Extensions to vite from CRA

Before moving forward, we must know, What is CRA and Vite? Why we should always use Vite over CRA? Let's have clarity to these questions first.

 

1. CRA (Create React App)

What it is:
CRA is a scaffolding tool by Facebook that lets you spin up a React app with zero config. It uses Webpack under the hood and provides out-of-the-box setup for Babel, ESLint, Jest, etc.

Pros (back when it launched):

  • Zero config setup for React.

  • Batteries included (build, dev server, testing).

  • Widely adopted → big community, lots of tutorials.

Cons (nowadays):

  • Webpack-based → slower builds, slower hot reload.

  • Large dev server memory usage.

  • Outdated: CRA is effectively unmaintained (Facebook stopped investing, React docs themselves suggest alternatives).

  • Harder to customize build without eject.

  • No built-in support for modern features like ESM-first, lightning-fast HMR, TS/JS mixing, etc.


2. Vite

What it is:
Vite (by Evan You, creator of Vue) is a next-gen frontend build tool. It uses esbuild for dev (super fast) and Rollup for optimized production builds.

Pros:

  • Instant dev server startup (no big bundle first, serves ESM on-demand).

  • Fast HMR (hot module replacement) — changes reflect almost instantly.

  • Lightweight (low memory usage).

  • First-class TypeScript support.

  • Built-in support for JSX/TSX, CSS, Sass, PostCSS without plugins.

  • Simple config (vite.config.js).

  • Supports code splitting, dynamic imports, tree-shaking out of the box.

  • Ecosystem: official plugins for React, Vue, PWA, SSR, etc.

Cons:

  • Smaller ecosystem than Webpack (but growing fast).

  • Sometimes plugin compatibility issues if you rely on niche Webpack loaders.


Why migrate from CRA to Vite?

  1. Performance

    • CRA (Webpack) = ~20s cold start, HMR delay.

    • Vite = <1s cold start, almost instant HMR.
      For larger apps, dev productivity boost is huge.

  2. Maintenance

    • CRA is no longer actively developed.

    • Vite is modern, actively maintained, and recommended by React docs for new projects.

  3. Better DX (Developer Experience)

    • Simple config.

    • Fast reloads.

    • Cleaner builds with Rollup.

  4. Future-proof

    • Vite is ESM-first (the future of JS).

    • CRA is stuck on older bundling approaches.

  5. Customizability

    • CRA requires eject to customize Webpack.

    • Vite lets you configure via plugins and vite.config.js without ejecting.


Overall Conclusion:

  • CRA was great in 2017–2019 for newcomers, but today it’s heavy, slow, and basically abandoned.

  • Vite is the modern replacement: fast dev server, instant HMR, simple config, actively maintained.

  • Migrating makes your project faster, lighter, and future-proof.


     

Now, as we have clear understanding of CRA and Vite. Let's move to the step by step migration process.

Quick prerequisites & naming (important)

  • Liferay 7.4+ uses Client Extensions defined by client-extension.yaml. You’ll reference your JS/CSS bundles there, and you’ll typically copy the built files into the CX ZIP via the assemble block.

  • Decide on the client extension ID / element tag once and reuse it everywhere. Example we’ll use below: my-react-custom-element (so the element is <my-react-custom-element />). Liferay’s official examples follow this pattern.


     

    Below is the battle-tested way to migrate a React Client Extension in a Liferay workspace from CRA (create-react-app) to Vite, with exact file changes and gotchas called out. This guide assumes you’re using a Custom Element client extension (the most common for React) inside a Liferay Gradle workspace.

  1. Replace CRA tooling with Vite

  • a) Remove CRA and add Vite

    package.json — dependencies & scripts (diff-style)

    
    
    

    If you prefer using Liferay’s shared React (v16) to shrink your bundle, keep reading; there’s an alt config in Step 4B. Otherwise, bundling React 18 inside your CE is the simplest path (and works great).


    2) Convert your HTML entry

    CRA ships public/index.html with placeholders (%PUBLIC_URL%, etc.). Vite uses a plain HTML file and injects scripts via <script type="module">.

    public/index.html → index.html (Vite likes it at project root):

    
    
    

    The element tag here must match the tag you register and the htmlElementName in YAML. (Liferay)


    3) Switch to a Custom Element entry file

    CRA’s default src/index.js renders into #root. While Vite's default src/main.jsx renders into #root. For Liferay CEs, register a web component and mount React inside. This pattern plays nicely with Liferay’s dynamic page lifecycle. 

    src/main.jsx (new)

    
    
    
    

    If your CRA app previously rendered to document.getElementById('root'), delete that code—Liferay may render/remove your CE many times and this pattern prevents memory leaks.


    4) Add Vite config (with Liferay-friendly base path)

    Vite needs to know the base URL that Liferay will serve your static files under. For Client Extensions, static assets are served from /o/<client-extension-id>. The official guidance sets base accordingly. (Liferay)

    4A) Simplest (bundle your own React 18)

    vite.config.js

    
    
    

    The base and outDir values align with the YAML you’ll set in Step 6. 

    4B) Smaller bundles (use Liferay’s shared React 16)

    If you must match Liferay’s shared React, externalize React and set classic JSX runtime (per Liferay blog):

    
    
    

    When externalizing React, align your package.json React version to 16 and rely on Liferay’s copy at runtime. Otherwise, stick to 4A.


    5) Environment variables & assets

  • Replace process.env.REACT_APP_* import.meta.env.VITE_* in your code.

  • Replace %PUBLIC_URL% usages with paths relative to import.meta.env.BASE_URL (or inline absolute/relative URLs).

  • If you imported SVGs as React components in CRA, add vite-plugin-svgr and keep your import Logo from './logo.svg?react' pattern (optional).

  • Static assets from CRA’s public/ go under Vite’s public/ (copied to the root of the build).


  • 6) Update client-extension.yaml

    This is how Liferay knows what to copy into the CX ZIP and what to load on the page. You’ll point assemble at Vite’s output folder and reference the built files using globs (hashes change per build). Liferay’s docs and examples use this exact pattern. (Liferay Learn, Liferay)

    client-extension.yaml

    
    
    
    

    useESM: true is correct because Vite emits ES modules. The urls/cssURLs paths are relative to the root of the ZIP (your static/ is the web root for the CE). 


    7) Local dev & API proxy (optional but nice)

    You can run npm run dev (or yarn dev) and load the CE in your own HTML shell (the index.html above). If you need to call Liferay APIs during local dev, set a proxy:

    
    
    
    

    Advanced: some teams live-mount the dev bundle in a Liferay page (HMR inside portal) using Vite’s backend-integration approach; that’s beyond basics but proven in the community.


    8) Build & deploy to Liferay

    From your client extension project folder:

    1) Build your Vite bundle
    npm run build

    2) Build the Client Extension ZIP with Gradle
    ../../gradlew build
    # outputs: dist/my-react-custom-element.zip

    3) Deploy into a local bundle
    ../../gradlew deploy

    After deployment, add the widget: Fragments & Widgets → Widgets → Client Extensions → My React Custom Element and drop it on a page. (Standard Liferay flow.) (Liferay, Liferay Learn)


    9) Typical file tree after migration

    
    
    

    10) Common CRA→Vite fixes

  • Env vars: rename REACT_APP_*VITE_*, access via import.meta.env.

  • PUBLIC_URL: replace with import.meta.env.BASE_URL or static paths.

  • Aliases: if you used NODE_PATH or CRA TS baseUrl, set in Vite:

    resolve: { alias: { "@": "/src" } }
    
  • Jest: CRA bundled Jest; for Vite, switch to Vitest (if you need tests).

  • Service worker / PWA: CRA’s SW needs replacement (e.g., vite-plugin-pwa).

  • SVG as components: add vite-plugin-svgr if you used that CRA feature.

  • React version:

    • Bundle your own React 18 (Step 4A) — simplest.

    • Or externalize and use Liferay’s React 16 (Step 4B) to shrink bundle.

  • If your “React client extension” is actually an iFrame remote app:

  • Build with Vite as a normal SPA (no custom element required).

Host the build (via CE static or external URL), then set CE type iFrame in YAML with the url to your app. Liferay docs cover Custom Element vs iFrame. (Liferay Learn)


Conclusion :

We have successfully migrated React Client Extensions built using CRA to Vite. In case, If your “React client extension” is actually an iFrame remote app:

  • Build with Vite as a normal SPA (no custom element required).

  • Host the build (via CE static or external URL), then set CE type iFrame in YAML with the url to your app. Liferay docs cover Custom Element vs iFrame.