In this guide, we’ll walk through how to build and deploy a React Client Extension in Liferay using Vite, while integrating Liferay’s global utilities like Liferay.ThemeDisplay
, Liferay.Language
, and others. This article is a complete upgrade to the previous “Beginner’s Guide” and includes practical examples.
Table of Contents
Why Vite?
Vite is a fast and modern frontend build tool that improves your DX (developer experience) with near-instant HMR, native ES module support, and an optimized build process. It’s perfect for building lightweight Liferay client extensions.
Step 1: Create React App with Vite
npm create vite@latest demo-react -- --template react
cd demo-react
npm install
Step 2: Modify vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
base: '',
build: {
outDir: 'dist',
assetsDir: './',
rollupOptions: {
input: 'index.html',
},
},
});
Step 3: Update index.html
Replace the root div with your custom element name (should match what’s in your YAML):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React CE</title>
</head>
<body>
<demo-react></demo-react>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Step 4: Update package.json
Add this section to prevent ESLint errors due to Liferay globals:
"eslintConfig": {
"globals": {
"Liferay": true
}
},
Step 5: Sample React Component
In App.jsx
, you can directly use Liferay’s global functions without optional chaining:
function App() {
return (
<div className="container">
<h2>Hello, {Liferay.ThemeDisplay.getUserName()}!</h2>
<p>Language: {Liferay.ThemeDisplay.getLanguageId()}</p>
<p>Group ID: {Liferay.ThemeDisplay.getScopeGroupId()}</p>
<p>{Liferay.Language.get('welcome')}</p>
<button
onClick={() => {
Liferay.Util.openToast({
message: 'This is a success toast!',
type: 'success',
});
}}
>
Show Toast
</button>
<button
onClick={() => {
Liferay.Util.navigate('/web/guest/home');
}}
>
Go to Home
</button>
</div>
);
}
export default App;
Step 6: Bootstrap via Custom Element in main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
class DemoReact extends HTMLElement {
connectedCallback() {
const root = ReactDOM.createRoot(this);
root.render(<App />);
}
}
if (!customElements.get('demo-react')) {
customElements.define('demo-react', DemoReact);
}
Step 7: Create client-extension.yaml
liferay-client-extension:
- name: demo-react
type: customElement
friendlyURLMapping: demo-react
htmlElementName: demo-react
portletCategoryName: category.client-extensions
urls:
- index.html
cssURLs: []
Make sure htmlElementName
and the tag in your index.html
match exactly.
Step 8: Build and Deploy
..\..\gradlew clean deploy
You can also use
blade deploy
if you’ve configuredblade.properties
to point to your Liferay bundle.
Final Output
Your widget will show the user name, language ID, and site group ID. You’ll also have buttons to show a success toast and navigate to another Liferay page — all React-powered and seamlessly integrated with Liferay’s APIs.
Best Practices
- Wrap all Liferay global calls in fallback/defaults for better error handling.
- Use
Liferay.on()
andLiferay.detach()
for event listeners. - Keep styles scoped if needed using CSS modules or inline styles.