← All posts
Liferay DXPReactClient ExtensionsViteDeveloper Experience

Instant Hot Reload for Liferay React Client Extensions

By LR Tools · August 23, 2026 · 6 min read

If you’ve built a React client extension for Liferay DXP, you know the loop: tweak a component, save, kick off a Gradle build, wait 20-30 seconds for it to deploy, then flip back to the browser and refresh to see whether the change actually did what you wanted. Do that fifty times in an afternoon of UI polish and you’ve spent most of your day watching a build log instead of writing code.

Pointing at a dev server closes half the gap

Liferay’s own documentation already describes a workaround: point your custom element at a local Vite dev server (typically http://localhost:5173) via client-extension.dev.yaml, instead of loading the built bundle. That removes the Gradle round-trip for JS/CSS changes, but it’s a manual setup with real gaps:

  • Custom elements still take a full page reload on every change, not a component-level update — so you lose whatever state you were mid-way through testing.
  • Wiring up Vite’s client script and the React Refresh runtime by hand is fiddly and easy to get subtly wrong.
  • A workspace with more than one client extension means tracking dev-server ports yourself, and it’s only a matter of time before two extensions collide on 5173.
  • Packages Liferay provides at runtime (auth helpers, Clay UI components) aren’t automatically excluded from the dev bundle, so you end up with duplicated copies of the same module — sometimes enough to make the page behave differently in dev than in production.

In short: closer, but not the standalone-React-app experience you actually want.

liferay-cx-hmr-setup: finishing the job

liferay-cx-hmr-setup is a CLI that takes over the parts of that setup you’d otherwise do by hand, across an entire Liferay workspace at once:

npx liferay-cx-hmr-setup

Running it against a workspace scans every client extension you have and configures each one for proper Hot Module Replacement — not just a dev-server redirect.

What it automates

Port allocation. It scans all of your client extensions, checks which ports are already taken, and assigns each one a free port in sequence (5173, 5174, 5175, …). No spreadsheet of “who owns which port,” no collisions when a new extension gets added.

External package resolution. A Vite plugin bundled with the tool recognizes Liferay-provided runtime packages — things like @liferay/oauth2-provider-web/client and @clayui components — and keeps them out of your dev bundle instead of duplicating them. Conceptually, that’s equivalent to telling Vite/Rollup:

// Illustrative — the CLI applies the equivalent externals for you
export default {
  build: {
    rollupOptions: {
      external: ['@liferay/oauth2-provider-web/client', /^@clayui\//],
    },
  },
};

Config generation. For each extension, the CLI generates or updates client-extension.dev.yaml, adds a small dev-only preamble script so the custom element boots correctly against the dev server, enables CORS in the Vite config so the DXP origin can load it, and makes sure scripts load in the right order.

Getting a workspace running

1. Run the CLI once per workspace:

npx liferay-cx-hmr-setup

2. Deploy the dev manifest — only needed when extension entries or port assignments change, not on every code edit:

./gradlew :client-extensions:my-cx:deployDev

3. Start developing:

npm run dev

From here on, editing a component updates it in place — with state preserved where React Refresh can manage it — and CSS edits apply instantly with no reload at all.

What actually changes day to day

Without HMR With liferay-cx-hmr-setup
Edit a component Save → Gradle build (~20-30s) → redeploy → refresh browser Save → update appears in place, usually under a second
Component state Lost on every reload Preserved where possible
CSS tweak Full rebuild cycle Applied instantly, no reload
Multiple extensions Manual port bookkeeping, collision-prone Ports auto-assigned and conflict-free

That difference compounds fast on anything with real UI iteration — a design review that used to mean twenty rebuild cycles becomes twenty edits you barely notice happening.

Production is untouched. Everything the CLI adds — the dev YAML, the preamble script, the Vite dev config — is dev-only. Your production client-extension.yaml and Gradle CI/CD pipeline don’t change, so there’s no risk of dev tooling leaking into a deployed build.

Takeaway

If your team ships React client extensions for Liferay DXP and is still living with the rebuild-and-redeploy loop, liferay-cx-hmr-setup is a single command that gets you to the sub-second feedback loop you’d expect from any standalone React app — without hand-rolling Vite config or babysitting port numbers across a multi-extension workspace.

This article is LR Tools’ adaptation of the original write-up by Ankit Hadiyal — read the full post on Liferay.dev for the team’s own framing.

This article is adapted from: Ankit Hadiyal, Liferay.dev

More from the blog

Liferay DXPPostgreSQLDatabase Migration

Migrating Liferay to PostgreSQL: A More Reliable Alternative to Liferay's Beta Tool

August 23, 2026 · 3 min read

Liferay's own database migration tool is still Beta and unreliable in practice. Here's a third-party alternative, the exact 8-step migration procedure, and links to the tool and its docs.

Liferay DXPSecurityCVE

When a CVE Isn't a Liferay Vulnerability: Presence vs. Reachability

August 23, 2026 · 7 min read

A security scanner finding a CVE in a bundled library isn't proof that Liferay DXP is exploitable through it. Here's how Liferay's security team tells the difference — and why an unnecessary dependency upgrade isn't automatically the safer choice.

Liferay DXPFree TierLicensing

DXP Free Tier Licenses: Why the Product Version Doesn't Have to Match Your Runtime

August 23, 2026 · 3 min read

A DXP Free Tier activation key stamped with a quarterly release that hadn't shipped yet looked like a bug. It wasn't — here's what the product-version field in a Liferay license actually means, and why it isn't a strict compatibility gate.