Liferay React Portlets

Creating a React portlet is easier than it has ever been.

Introduction

A client asked me to build a React portlet for them and I was like, "Oh Yeah!" I mean, it's been a while since I built a React portlet and tooling has really changed a lot since then, so I was really looking forward to this new task.

So without further ado, I'm just going to dive right in...

Creating the Module

Okay, so this is still Liferay and still OSGi, so we're going to be building a module.

You have basically two options:

1. Use the Yeoman generator-liferay-js generator. This is the path to take when you are not already using a Liferay Gradle Workspace. After you install it, use the yo liferay-js command to start the generator. If this is the route you want to take, first install yo and the generator using the command npm install -g yo generator-liferay-js and then you can use the yo liferay-js command.

2. Use Blade to create a liferay-js module. This actually uses the same Yeoman generator, but it will create your module with the necessary stuff to allow the Gradle workspace to build your module. Command to create the module is blade create -t js-widget my-module-name. If you have Blade installed, you don't need anything more.

There's actually two other legacy options that I'll mention but not go further with. The first is another Yeoman generator, the generator-liferay-bundle. This was prevalent at some point (that I think I missed entirely), but now it is deprecated. Also, in Blade there is the npm-react-portlet module type which actually builds a larger Java portlet around the launch of the React code. This can be handy if you need something like defined portlet permissions. I feel like this one is going to be a larger by-hand responsibility for building the React code than using the standard React tooling, but I don't know this for certain.

Deciding between which of the options to go with comes down to an answer to a simple question: Are you also using a Liferay Gradle Workspace? If yes, go with #2 as you can include your React portlet build in with your other customizations. If no, go with #1 and you can build your project separately.

And if you're a Maven person? Well, um, yeah... For you I'd probably try to do option #1 on its own, if possible. Maven is capable of invoking npm to do a build, though, so it should be possible to use #1 to create a module in your Maven workspace and then add some pom magic to complete the build, but I'm afraid you're on your own there. If someone wants to share a pom in the comments, that would be awesome!

Invoking the liferay-js Generator

Regardless which option you pick, it will always end up running the liferay-js generator.

The generator is going to prompt you a number of times for values, this is what you'll see:

? What type of project do you want to create? React Widget
? What name shall I give to the folder hosting your project? modules/my-module
? What is the human readable description of your project? My React Portlet
? Do you want to add localization support? Yes
? Do you want to add configuration support?

  Needs Liferay DXP/Portal CE 7.1 with JS Portlet Extender 1.1.0 or
     Liferay DXP/Portal CE 7.2+.

 Yes
? Under which category should your widget be listed? category.react
? Do you have a local installation of Liferay for development? Yes
? Where is your local installation of Liferay placed? ~/liferay/react/bundles
? Do you want to generate sample code? Yes

The first prompt is for the JS portlet type. You can pick JavaScript, Angular, React, Vue.js or Metal.js. Obviously I chose React for my portlet.

Next is the folder where your code will be placed. If outside of a workspace, you can pick anything, but in the workspace it will use the modules directory and the my-module name which you specified on the blade command line.

The human readable description is really just your friendly portlet name.

You have the option of enabling Localization for your portlet. Most of the time I opt to support localization.

The next option is to enable configuration support. I was a bit thrown when I saw this prompt the first time because you answer underneath the note, I didn't realize it was connected to the question above. I like configuration options, so I chose yes. The note just tells you the minimum requirements for config support, so you can't use this in 7.0 or 7.1 without the JS portlet extender.

The next prompt is for the category where your portlet will appear in the widgets menu. "category.name" is standard and can then be handled with localization.

Next two questions are about your local dev bundle. If you are not using the workspace, you'll need to pick your path. If you are using the workspace, it assumes you're going to use the workspace bundle, but you can change it if necessary.

Finally, you can generate the sample code in your new portlet. Actually I'd suggest always saying yes to this prompt as it starts your localization and config for you, and basically lays a solid foundation for your portlet without introducing any real cruft.

After this last question, the generator will create your new module folder.

Anatomy of the Liferay JS React Portlet

After the code has been generated, you'll have this structure:

$ tree my-module
my-module
├── README.md
├── assets
│   └── css
│       └── styles.css
├── build.gradle
├── config.json
├── features
│   ├── configuration.json
│   └── localization
│         └── Language.properties
├── package.json
└── src
    ├── AppComponent.js
    └── index.js

5 directories, 8 files

This is even with the sample data thrown in!

The README.MD is just what you think it is. It starts out being blank, but I love to use this to add documentation about my react portlet. For example, the one I did for the client had some SB remote services thrown in, so in the README I included the code for calling the right services and the kinds of JSON I got back as a result. It can be a handy place to keep notes like this.

The assets folder will include hard assets like css files, images, etc.

build.gradle I got because I'm in a workspace, but it is empty if you check it.

config.json is configuration for the module itself. If you check it you'll find it includes module and build details.

The features folder holds the config and/or localization stuff. Localization is the standard Liferay resource bundle stuff, just know that it will be copied into the right spot at build time to make all of your localization strings work as expected. The configuration.json I'm going to cover in more detail in a coming section.

The package.json is the file every node developer recognizes. There are some things in here that are Liferay-specific, including all of the portlet properties we know from the portlet.xml and liferay-portlet.xml from the past or Java portlet @Component annotations from the present. I wouldn't tamper with sections in here you don't understand, otherwise you could foul your build.

And finally, the src folder is where your React source is going to go.

Configuration.json File

I wanted to call this one out because honestly I couldn't find details on it anywhere else...

Javascript portlets like the React one I'm building can now access configuration properties from the Liferay system scope (i.e. global System Settings control panel) or portlet instance scope (the configuration panel from a typical Liferay portlet). Pretty cool, huh?

By defining your configuration in the configuration.json file, the Liferay UI will present your configuration dialog in the two places (System Settings or the portlet configuration dialog) for gathering config details, plus it will pass those details off to the portlet so you can leverage the configuration values in the portlet.

The file is defined by a JS schema, specifically this one: https://raw.githubusercontent.com/liferay/liferay-js-toolkit/master/resources/schemas/configuration.schema.json. It is kind of hard to read if you haven't seen one before, so I'll break it down here... There is a useful reference page here: https://github.com/liferay/liferay-frontend-projects/blob/master/maintenance/projects/js-toolkit/docs/configuration.json-file-reference.md

There are two primary objects, the system object and the portletInstance object. Each contain the config items for the target scopes, either the System Settings or the portlet configuration dialog.

For the system object, there are two attributes, category and name which will be the category and name you'll see in the System Settings page. The portletInstance object does not need these attributes.

The fields object is part of both sections and contains the configuration key/definition pairs. The key is the configuration field name, it is what you'll reference in your JS code to get the value of the config item.

The definition is an object that contains a lot of items:

  • type is the configuration type and can be one of number, float, string or boolean for both system and portletInstance items, but system can have an additional password type that portletInstance cannot. Before you ask, the answer is No, there is no way to add additional types to the configuration.
  • Name and description are either the human readable values or the language bundle keys for the labels that will appear in the UI.
  • default is the default value for the config item in case it has not been set. Be sure to use a value consistent with the type.
  • options is an object of key/label pairs used when displaying a pick list. The key will be what the value of the configuration item is, and the label is what the user will see (either human readable or language bundle key work here). You only want to use the options when you are limiting to a list of items for the user to pick from; if you leave it off, they will see a basic text entry field.

Your React components will have access to the configuration through the props.configuration object. So the sample data gives you two configuration fields, one "fruit" and one "drink". You could initialize your state using these values:

state = {
  favoriteFruit: props.configuration.system.fruit,
  favoriteBeverage: props.configuration.portletInstance.drink
};

You can also reference them in the props argument on your constructor if you have one.

Using Clay Components

We all love a good component library, right? For Liferay, one component library you might want to pick is ClayUI. Why? Well consistency is a good reason, right? Liferay as a whole is leveraging Clay all over the place, so if you use the Clay components, your portlet should be visually consistent with the rest of Liferay.

ClayUI is Liferay's component suite that is used for all of the out of the box portlets and can also be used in your React portlets. See the whole suite here: https://clayui.com/

So how do you add the Clay components to your project? It's pretty easy actually... First, you want to find the list of components here: https://clayui.com/docs/components/index.html. When you find the component you want, have npm install it like I've done here for the ClayUI Table component:

$ npm install @clayui/table

Now if you see a warning (like you would for table) about not finding the peer ClayUI CSS component, you can just ignore that. And don't try to install the ClayUI CSS component either. The ClayUI CSS is already going to be loaded in as part of the theme.

Because ClayUI CSS is part of the theme, this can pose a problem when rendering ClayUI components. The install command I've provided is going to try to use the latest version of the ClayUI component, but there could be something missing because the theme would have an older version of the ClayUI CSS component. If you find this happening with a ClayUI component you're using, try to use an older version until you get one that is inline with the ClayUI CSS in the theme.

You can selectively add ClayUI components to your module, or you can just add them all using the following:

$ npm install @clayui/alert @clayui/autocomplete @clayui/badge @clayui/breadcrumb \
  @clayui/button @clayui/card @clayui/charts @clayui/color-picker \
  @clayui/data-provider @clayui/date-picker @clayui/drop-down @clayui/empty-state \
  @clayui/form @clayui/icon @clayui/label @clayui/link @clayui/list \
  @clayui/loading-indicator @clayui/management-toolbar @clayui/modal \
  @clayui/multi-select @clayui/multi-step-nav @clayui/nav @clayui/navigation-bar \
  @clayui/pagination @clayui/pagination-bar @clayui/panel @clayui/popover \
  @clayui/progress-bar @clayui/shared @clayui/slider @clayui/sticker @clayui/table \
  @clayui/tabs @clayui/time-picker @clayui/tooltip @clayui/upper-toolbar

With these pulled in, you can then start importing components into your React files:

import ClayTable from "@clayui/table";
import Checkbox from "@clayui/form";

And also using them in your render() methods:

render() {
  return (
    <div className="my-table">
      <ClayTable>
        <ClayTable.Head>
          <ClayTable.Row>
            ...

The ClayUI site should have all of the details you need to leverage the components effectively in your React portlets.

Liferay JavaScript Object

Liferay has a suite of Javascript objects and services at your disposal, even if you are using React.

I spoke of some remote SB services I had created and used as part of one of my projects, for example. The way to invoke these kinds of services (and you'll find examples like this scattered throughout the Liferay OOTB portlets), is via code like:

_getEntries(callback) {
  var instance = this;

  Liferay.Service(
    '/assettag/get-groups-tags',
    {
      groupIds: instance.get('groupIds'),
    },
    callback
  );
}

You can still do this in your React code, too!

But wait, you might ask, why aren't you using OAuth2 Dave? Honestly, because I don't have to. I'm already authenticated into Liferay, the Liferay.Service() call is going to add my p_auth parameter so Liferay will verify I'm already logged in, plus I find I can easily code up a remote SB service easy enough. If I were doing a React SPA or exposing services for some other outside consumer, the new RESTBuilder stuff is the way to go.

Liferay has lots of other JS available such as fire() and on() for InterPortlet Communication (IPC), that works even with React code. You can access Session object to extend the session, you can access the themeDisplay object, ... Anything that's there, you can call it just by using the Liferay container.

Routing

If you have a really simple React portlet, routing may not be necessary.

Most of the time, though, you probably want to have routing so you can navigate within your portlet. When you do, you can't just use any Router implementation. When running under Liferay, Liferay owns the address bar, not your React portlet.

Avoid BrowserRouter at all costs. Instead, opt for the HashRouter (puts your routing stuff after a hash # on the url, Liferay will ignore that) or even better the MemoryRouter.

I always recommend the MemoryRouter because it is the best option since (a) Liferay owns the address bar and (b) two React portlets on the same page will trip over each other if they both use the HashRouter. The Liferay documentation, however, leans towards the HashRouter as it is the only address bar modification that Liferay supports. The caveat here is that you cannot have multiple JS-based portlets on the page each trying to use hash-based routing, they will clobber each other and break the routing for all.

Using MemoryRouter is pretty simple, just start with the import:

import React from 'react';
import {
	NavLink,
	Redirect,
	Route,
	MemoryRouter as Router,
	Switch
} from 'react-router-dom';

With that in, you can build out your app with your navigation routes defined:

import React from 'react';
import {
	NavLink,
	Redirect,
	Route,
	MemoryRouter as Router,
	Switch
} from 'react-router-dom';

import EditValues from './pages/page1/Edit';
import Home from './pages/index';
import Page1 from './pages/page1/index';
import Page2 from './pages/page2/index';
import Page2Create from './pages/page2/Create';

class App extends React.Component {
  render() {
    return (
      <Router>
        <div>
          <nav className="navbar navbar-expand-lg navbar-light bg-light">
            <ul className="navbar-nav">
              <li className="nav-item">
                <NavLink activeClassName="active" className="nav-link" exact 
                  to="/">Home</NavLink>
              </li>
              <li className="nav-item">
                <NavLink activeClassName="active" className="nav-link" 
                  to="/page1">Page1</NavLink>
              </li>
              <li className="nav-item">
                <NavLink activeClassName="active" className="nav-link" 
                  to="/page2">Page2</NavLink>
              </li>
            </ul>
          </nav>

          <Route component={Home} exact path="/" />
          <Route component={Page1} exact path="/page1" />
          <Route component={Page2} exact path="/page2" />
          <Route component={Page2Create} exact path="/page2/create" />

          <Switch>
            <Route component={EditValues} exact path="/page2/create" />
            <Route component={EditValues} exact path="/page2/:id/edit" />
          </Switch>
        </div>
      </Router>
    );
  }
}

export default App;

Just follow the regular React rules for routing and you're good to go.

Unmounting the React Portlet

This is an addition from information provided by my friend Julien Mourad: If you do not have SPA disabled (you'll know if you did disable SPA), it is important that you unmount your React portlet.

With SPA enabled, Liferay does not do a full page refresh, it's doing AJAX calls back to Liferay and performing partial page updates, even when you navigate from page to page.

With SPA enabled, your React portlet will never get unmounted automatically as the user navigates around on pages, you have to handle the unmounting manually.

Fortunately that's pretty easy to do. In the index.js of your portlet, you're going to add 3 lines of code to the main() function. Here's how it looks for the sample code generated when the module was created:

export default function main({portletNamespace, contextPath, 
          portletElementId, configuration}) {
    ReactDOM.render(
        <AppComponent 
            portletNamespace={portletNamespace} 
            contextPath={contextPath}
            portletElementId={portletElementId}
            
            configuration={configuration}
            
            />, 
        document.getElementById(portletElementId)
    );
    
    // Liferay will issue the destroyPortlet event when the
    // portlet should unmount.
    Liferay.once('destroyPortlet', () => {
        ReactDOM.unmountComponentAtNode(container);
    });
}

The last three lines are the ones that were added to trigger the unmount() call.

Although these lines are not needed when SPA is disabled (the full page refresh will always ensure the browser memory has been reset on every page render), I would actually recommend just adding them to all of your React portlets.

After all, you don't really know if an admin will come along and either enable or disable SPA without telling the developers. Adding these three lines will ensure that you're unmounted whether SPA is enabled or not.

Building the React Portlet

Building your React-based portlet couldn't be easier.

If you're using the standalone yo liferay-js option, well you have an NPM project ready to create the portlet jar and you just use the npm run build command. If you're using the Liferay Gradle Workspace, just do your regular gradlew build and it does the rest.

In both cases, you'll have two new folders in the root of your project. First is the build dir where all of the raw artifacts are in the appropriate locations.

The second is the dist folder. In here, you'll find your React portlet jar file, ready to be plucked and sent off to a remote Liferay server for deployment.

For your own development, you can use npm run deploy or gradlew deploy and your React portlet will be deployed to the local Tomcat bundle you pointed at when creating the module using the generator.

Conclusion

So this is it, really. I know, I know, there's not a lot of React in here, but there doesn't need to be. The info here gives you the framework to build the next great React portlet, but that's gotta come from you. Using the liferay-js generator you have a solid foundation, you just need to add inspiration and some React code.

Thats all I have. If you have any React portlet tips you'd like to share, please add a comment below. If you have a question or comment, add those below too. If you find a mistake in what I wrote, keep it to yourself... Just kidding, I'd love your feedback below and will fix the content. Or of course you can find me in the community Slack and chat with me directly.

3
Blogs

Hi David,

thanks for this post. But, we have some issues with Liferay 7.4.1-GA2 and the react portlet configurations. Please look at this question: https://liferay.dev/ask/questions/development/liferay-7-4-1-ga2-reactjs-portlet-configuration-could-not-persisted

We use the react portlets generated with 'yo' since 1 year. With Liferay 7.3.x the configuration are saved in the DB and they work fine. But in Liferay 7.4.1 it doesn't. The configuration are not stored in DB (postgres 11 with Liferay 7.3.x; postgres 12 with Liferay 7.4.1) 

We have generated a fresh 'testing-like' portlet by your instructions in this post. With the sample code and the sample configuration. And the configuration could not be stored in DB! We need the possibilty of the portlet configurations. But, now we can't use this types of portlets with Liferay 7.4.1. What we do wrong?

BTW: If we try to generate the portlet with blade (version 4.0.9.202107011607), we get the following error: 'create: liferay-js is not among the possible values.'

Hello David,

Thanks for this post.

We can also call the Liferay REST API usin Liferay.uil.fetch like this exemple :

const getMyUserAcount = async()=>{     try {         const result = await Liferay.Util.fetch(`/o/headless-admin-user/v1.0/my-user-account`, {method: 'GET'});         return result.json();     } catch (error) {         console.log(error);             } }

const displayCurentUser = async () => {     try {                  const result = await getMyUserAcount();         const {givenName, familyName} = result;         console.log("Hello " + givenName +" "+ familyName);              } catch (error) {         console.log(error);     } }

displayCurentUser();