Recently I took on the challenge of building a React app that utilizes Typescript & Material UI by using the Liferay-JS Yeoman generator. This approach was taken as an alternative to using Create React App & adapting it to Liferay. The benefit is there are no extra steps needed to gain access to the Liferay Params (configuration, contextPath, portletElementId, & portletNamespace) as there would be in an CRA adaptation.

Here are the steps I have found that, at the time of writing this blog, are working for me…

**NOTE – You must use NPM & not Yarn. Using Yarn causes errors when bundling MUI components. Also, this is written using DXP 7.3**

1. Create a React App using the yeoman liferay-js generator. If you have not globally installed yeoman & the generator-liferay-js npm packages, follow the links & instructions below.

2. Once the App has been created install the following dependencies:

  • Dev dependencies
    • @babel/preset-typescript
    • liferay-npm-bundler-plugin-inject-imports-dependencies
npm i -D @babel/preset-typescript liferay-npm-bundler-plugin-inject-imports-dependencies
  • Standard dependencies
    • typescript
    • @types/node 
    • @types/react 
    • @types/react-dom 
    • @types/jest (optional for testing)
    • @types/material-ui
    • @material-ui/core
    • @material-ui/icons
    • @material-ui/styles
    • @material-ui/lab (optional)
npm i typescript @types/node @types/react @types/react-dom @types/jest @types/material-ui @material-ui/core @material-ui/icons @material-ui/styles @material-ui/lab

3. Run ‘tsc –init’ in your root folder to create a tsconfig.json. The following tsconfig.json works at the time of writing this:

{
    "compilerOptions": {
      "target": "ES5",
      "module": "ESNext",
      "lib": [
        "es6",
        "dom",
        "dom.iterable",
        "esnext"
      ],
      "allowJs": true,
      "checkJs": false,
      "jsx": "react-jsx",
      "declaration": true,
      "noEmit": true,
      "isolatedModules": true,
      "strict": true, 
      "noImplicitAny": true,
      "strictNullChecks": true,
      "noImplicitThis": true,
      "noFallthroughCasesInSwitch": true,
      "moduleResolution": "node",
      "allowSyntheticDefaultImports": true,
      "esModuleInterop": true,
      "skipLibCheck": true,
      "forceConsistentCasingInFileNames": true,
      "resolveJsonModule": true
    },
    "include": [
      "src/**/*"
    ],
    "exclude": [
      "node_modules"
    ]
}  

4. Update your .babelrc to the following:

{
    "presets": [
        "@babel/preset-env", 
        "@babel/preset-react",
        [
            "@babel/preset-typescript",
            {
                "isTSX": true,
                "allExtensions": true
            }
        ]
    ]
}

5. Update your .npmbundlerrc to the following. This is so Material UI components will work w/ the Liferay generated bundle:

{
    "create-jar": {
        "output-dir": "dist",
        "features": {
            "js-extender": true,
            "web-context": "/exercise1.1",
            "localization": "features/localization/Language",
            "configuration": "features/configuration.json"
        }
    },
    "dump-report": true,
    "*": {
        "plugins": ["inject-imports-dependencies"]
    }
}

6. Update the build script & add the check-types script to your package.json. Adding ‘check types’ will make sure there are no TS errors before Babel complies. If there are errors the compile step will not happen:

"build": "npm run check-types && npx babel src -d build src --extensions '.ts,.tsx' && npm run copy-assets && liferay-npm-bundler",
 "check-types": "tsc",

7. You should now be able to use Typescript & Material UI in your Liferay generated React App. Rename your index & AppComonent files from .js to .tsx. You will have to create types for the Liferay generated props. As a shortcut here is my index.tsx file:

import React from 'react';
import ReactDOM from 'react-dom';
import AppComponent from './AppComponent';
 
 
interface IProps {
    portletNamespace: string;
    contextPath: string;
    portletElementId: string;
    configuration: any;
}
 
/**
 * This is the main entry point of the portlet.
 *
 * See https://tinyurl.com/js-ext-portlet-entry-point for the most recent 
 * information on the signature of this function.
 *
 * @param  {Object} params a hash with values of interest to the portlet
 * @return {void}
 */
export default function main({portletNamespace, contextPath, portletElementId, configuration}: IProps){    
    ReactDOM.render(
        <App 
            portletNamespace={portletNamespace} 
            contextPath={contextPath}
            portletElementId={portletElementId}
            configuration={configuration}
            />, 
        document.getElementById(portletElementId)
    );
    
}

And AppComponent.tsx:

import React from 'react';
 
declare global {
    interface Window {
        Liferay: any;
    }
}
 
interface IProps {
    portletNamespace: string;
    contextPath: string;
    portletElementId: string;
    configuration: any;
}
 
const Liferay: any = window.Liferay;
 
const AppComponent: React.FC<IProps> = (props: IProps) => {
    return (
        <div>
            <div>
                <span className="tag">{Liferay.Language.get('portlet-namespace')}:</span> 
                <span className="value">{props.portletNamespace}</span>
            </div>
            <div>
                <span className="tag">{Liferay.Language.get('context-path')}:</span>
                <span className="value">{props.contextPath}</span>
            </div>
            <div>
                <span className="tag">{Liferay.Language.get('portlet-element-id')}:</span>
                <span className="value">{props.portletElementId}</span>
            </div>
            
            <div>
                <span className="tag">{Liferay.Language.get('configuration')}:</span>
                <span className="value pre">{JSON.stringify(props.configuration, null, 2)}</span>
            </div>
        </div>
    );
}
 
export default AppComponent;

8. Now deploy the app to your local Liferay & if everything was setup right you should see something akin to the following (names will be different) after adding your new app to a content page:

Image Building a Liferay JS Generated React App deploy local Liferay

9. At this point, you can start building your app & adding Material UI components. As stated in the note at the top make sure you are using npm & not yarn. Somewhere in your node_modules yarn installs a second instance of React which causes an Invariant Violation: Invalid hook call error after compiling the code & running in Liferay.

References:

Babel preset typescript:

https://babeljs.io/docs/en/babel-preset-typescript

Babelrc config for TS:

https://stackoverflow.com/questions/63143214/compiling-tsx-to-js-with-babel-not-correct

Creating a ‘check-types’ script & other Babel/TS info:

https://iamturns.com/typescript-babel

How to implement material-ui on Liferay:

https://cnpmjs.org/package/liferay-npm-bundler-plugin-inject-imports-dependencies

Material UI w/ TS:

https://material-ui.com/guides/typescript

Use NPM & not Yarn:

https://github.com/mui-org/material-ui/issues/12934#issuecomment-435016750