Creating React Client Extensions in Liferay Using Vite (with Liferay Globals)

Liferay Version: 7.4

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.

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 configured blade.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() and Liferay.detach() for event listeners.
  • Keep styles scoped if needed using CSS modules or inline styles.

Also Read

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

close
Thanks !

Thanks for sharing this, you are awesome !