Upgrade to v4

Contents

What

Graal.JS

We have dropped support for Nashorn in favour of Graal.JS.

This means we had to upgrade the Enonic XP version requirement to 7.12 (gradle.properties)

Since the Application Configuration setting react4xp.ssr.settings was only relevant for Nashorn, it has been removed.

And since React4xp now only supports Graal.JS: the Application Configuration setting react4xp.ssr.engineName is no longer relevant.

TypeScript

React4xp version 4 introduces support for TypeScript

The types for lib-react4xp are available via npm:

See the tsconfig.json section below on how to set it up.

React 18

React4xp will now use the new React 18 functions like createRoot() and hydrateRoot() if they are available.

If they are not, it will use the React 17 functions like render() and hydrate().

Target folder (browser cache)

We have changed target folder from build/resources/main/assets/react4xp to build/resources/main/r4xAssets. The React4xp assets are currently provided by a service on top of lib-static and should not be available via assetUrl. When building in production mode: the asset filenames contain contenthashes, so they can be served as static assets with "infinite" browser caching. When building in development mode, React4xp uses ETag.

We have not changed the source folder src/main/resources/react4xp, so you don’t have to move any files.

Named exports

The default export from '/lib/enonic/react4xp' used to be a class. Now we are using named exports. You can still import the class exported under the name React4xp.

clientRender (hydrate & ssr)

The clientRender option/parameter has been removed in favour of hydrate and ssr.

Globals vs Externals

We have added a new configuration option to PROJECT_DIR/react4xp.config.js called globals. Whatever you had in externals should probably be moved to globals.

React4xp builds a globals bundle, which MUST contain all assets NEEDED to render server-side. By default it contains react and react-dom, but more assets can be added.

By default the globals bundle is also used on the client-side, but you can disable the serving of the globals bundle to the client-side in the Application configuration and provide the REQUIRED assets on your own, for example via CDN.

It is possible to have pure client-side rendered components in React4xp. If these components use assets which are NOT needed to render server-side you should add them to externals and not globals. This way those assets will not be bundled and must be provided by other means, for example via CDN.

Globals Externals

What is it?

A javascript bundle of commonly required imports.

A list of imports to be excluded.

Where is it used?

Always server-side, and sometimes client-side.

Build-time to exclude files.

How it works?

Instead of bundling a copy of the global imports into every target file, they are put in a separate bundle, and the target files will only contain a require statement to the globals bundle. During runtime the globals bundle is loaded first so the imports can be accessed directly on the global namespace.

Your code will assume these imports are available in the global namespace, but unless you also load the imports first, your code will throw errors.

Lazy loading (eager removed)

React4xp now supports pure clientSide rendered components. Such components may include code that doesn’t run on the server. Since eager loading would load all components, it would cause errors on the server (sometimes severe). Lazy loading only loads the components needed for the current serverSide rendering, not wasting cpu cycles on loading code it may never run.

Since lazy loading is now the default, and there is no eager loading anymore we have also removed the react4xp.ssrLazyload setting from the application configuration file (app.cfg)

Application configuration

We have added three new application configurations (hydrate, serveGlobals and ssr), and removed a few ones too (ssr.engineName, ssr.lazyLoad and ssr.settings).

react4xp.hydrate

Add this to to your application configuration file:

${XP_HOME}/config/${app}.cfg
react4xp.hydrate = false

To disable client-side hydration for all React4xp components (page, layout, parts, etc)

You can still override the application configuration in specific controllers.

someController.ts
  render(compnent, props, request, {
      // Default is to use application configuration
      // SSR without hydration will always be enforced when request.mode === 'edit'
      // hydrate: true // Hydration when ssr = true
      // hydrate: false // No hydration even when ssr = true
  })

react4xp.serveGlobals

To disable serving the globals bundle to the client-side add the line below to your application configuration file:

${XP_HOME}/config/${app}.cfg
react4xp.serveGlobals = false

react4xp.ssr

Add this to your application configuration file:

${XP_HOME}/config/${app}.cfg
react4xp.ssr = false

To disable server-side rendering for all parts (doesn’t affect page and layouts)

You can still override the application configuration in specific part controllers.

partController.ts
  render(compnent, props, request, {
      // Default is to use application configuration
      // SSR without hydration will always be enforced when request.mode === 'edit'
      // ssr: true // "Always" SSR
      // ssr: false // "Always" client-side rendering
  })

react4xp.ssr.lazyLoad

Lazyloading is now always enabled.

Therefore this setting has been removed.

Multiple React4xp apps

You can now use components from multiple React4xp apps on the same webpage.

By default React4xp apps serve a globals bundle to the browser. This globals bundle contain react (and more). If multiple copies of react are loaded in the browser, you will probably run into problems:

Luckily there is a workaround:

You can use Application Configuration to disable the serving of the globals bundle.

react4xp.serveGlobals = false

The challenge then is that you have to provide globals (including react and react-dom) manually. Either using the assets folder, lib-static or from CDN.

You could let one of the apps keep react4xp.serveGlobals = true, but then you would have to use a component from that app on any page that uses components from any of the react4xp apps.

We are working on a better solution for this complexity.

Client & Executor

To enable multiple React4xp applications on the same page global variables has been prefixed with the application name. Obviously the application name is not available when bulding lib-react4xp, so the building of the client and executor has been moved from lib-react4xp to @enonic/react4xp.

Content Studio

SSR without hydration used to be enforced for both edit and inline mode in Content Studio.

Now React4xp will only enforce SSR without hydration in Content Studio edit mode.

inline mode will use normal React4xp rendering, just as preview and live mode.

If you don’t pass on the request to the render functions React4xp will assume edit mode.

Gradle

We have simplified the gradle setup a lot.

All old references to React4xp in the build.gradle file, must be removed.

These files no longer exist:

  • node_modules/@enonic/react4xp/react4xp.gradle

  • node_modules/@enonic/react4xp/npmInstall.gradle

  • node_modules/@enonic/react4xp/updaters.gradle

See more under the build.gradle section below.

Enonic XP dev mode

When running Enonic XP in dev mode, it may be faster to build without using gradle at all.

See the required changes to the build.gradle and package.json files in the How section below.

System environment variables

When building with gradle, it will automatically set some system environment variables for you.

However if you want to build without using gradle you have to set them up on your own.

These two are required:

  • R4X_APP_NAME (find the appName in gradle.properties)

  • R4X_DIR_PATH_ABSOLUTE_PROJECT (cwd/pwd)

These two are optional:

  • R4X_BUILD_LOG_LEVEL (use INFO to get some extra logging when building)

  • NODE_ENV (the default is production, set it to development for no hashing, nor minification, etc…​)

Component props (react4xpId)

React4xp used to add an extra prop called react4xpId, which was used during clientSide hydration and rendering. This prop is no longer needed as it is provided by other means (script[data-react4xp-ref]).

So now: React component props are just normal React component props :)

document polyfill

Several frameworks and node modules uses document to determine whether it’s code is running on the server, or in the browser. By polyfilling document that logic is broken. So React4xp is no longer polyfilling document.

If your code is using document, and is not a pure client-side component: you should wrap the code with an if block to avoid that code being run on the server.
If you are importing some "broken" module that uses document without checking for client or server, you may polyfill document on your own, but it might break other modules which now thinks the server is the client…​

How

gradle.properties

Set xpVersion to 7.12.0 or higher:

gradle.properties
xpVersion = 7.12.0

build.gradle

build.gradle
dependencies {
    include "com.enonic.lib:lib-react4xp:4.x.x"
}

Remove all the old react4xp* tasks from your build.gradle file.

Add this one task instead:

build.gradle
task react4xp(type: NpmTask, dependsOn: npmInstall) {
  args = [
    'run',
    'build:react4xp' // This script must exist in the package.json file
  ]
  description 'Compile React4xp resources'
  environment = [
    'R4X_APP_NAME': "${appName}",
    'R4X_BUILD_LOG_LEVEL': gradle.startParameter.logLevel.toString(),
    'R4X_DIR_PATH_ABSOLUTE_PROJECT': project.projectDir.toString(),
    'NODE_ENV': project.hasProperty('dev') || project.hasProperty('development') ? 'development' : 'production'
  ]
  group 'react4xp'
  // It also watches package.json and package-lock.json :)
  inputs.dir 'node_modules/@enonic/react4xp'
  inputs.dir 'src/main/resources'
  outputs.dir 'build/resources/main'
}
jar.dependsOn 'react4xp'

If your project is based on an earlier version of the starter-react4xp also remove the react4xp plugin:

build.gradle
plugins {
  id 'react4xp' // Delete this line
}

You can probably also delete the entire buildSrc folder from your project.

package.json

When runnning Enonic XP in dev mode, it’s possible to build without using gradle.

In order to build without gradle we had to move npm explore command from build.gradle to the package.json file:

package.json
{
  "scripts": {
    "build:react4xp": "npm explore @enonic/react4xp -- npm run build:react4xp",
  }
}

Install or upgrade the React4xp build system:

npm install --save-dev @enonic/react4xp
npm upgrade @enonic/react4xp

react4xp.config.js

react4xp.config.js
  // Used in ssr component(s)
  globals: {
    lodash: '_'
  },
  // Used in pure clientSide component(s).
  // Must be provided by other means, for example CDN.
  externals: {
    jquery: 'jQuery'
  },

app.cfg

Hydration is enabled by default, to change the default to disabled add the line below to ${XP_HOME}/config/${app}.cfg. One can still enable hydration in specific components.

react4xp.hydrate = false

SSR is enabled by default, to change the default to disabled add the line below to ${XP_HOME}/config/${app}.cfg. One can still enable ssr in specific components.

react4xp.ssr = false

To disable serving the globals bundle to the client-side add the line below to ${XP_HOME}/config/${app}.cfg.

react4xp.serveGlobals = false

import/require in controllers

TypeScript named function

examplePart.ts
import {render} from '/lib/enonic/react4xp';

export function get(request) {
    return render(component, props, request, {
      // Optional
      // hydrate: false,
      // ssr: false
    });
}

TypeScript class

examplePart.ts
import {React4xp} from '/lib/enonic/react4xp';

export function get(request) {
  const r4x = new React4xp(jsxPath);
  r4x.setId(id);
  r4x.setProps(props);
  return {
    body: r4x.renderBody({
      body,
      request,
      // ssr, // Optional
    }),
    pageContributions: r4x.renderPageContributions({
      // hydrate, // Optional
      pageContributions,
      request,
      // ssr, // Optional
    })
  };
}

Common.JS "named" function

examplePart.js
const libReact4xp = require('/lib/enonic/react4xp');

exports.get = function (request) {
    return libReact4xp.render(component, props, request, {
      // Optional
      // hydrate: false,
      // ssr: false
    });
}

Common.JS class

examplePart.js
const libReact4xp = require('/lib/enonic/react4xp');

exports.get = function (request) {
    const r4x = new libReact4xp.React4xp(jsxPath);
    r4x.setId(id);
    r4x.setProps(props);
    return {
      body: r4x.renderBody({
        body: body,
        request: request,
        // ssr: ssr, // Optional
      }),
      pageContributions: r4x.renderPageContributions({
        // hydrate: hydrate, // Optional
        pageContributions: pageContributions,
        request: request,
        // ssr: ssr, // Optional
      })
    };
}

tsconfig.json

TypeChecking for your code editor (tsconfig.json)

Install some types:

npm install --save-dev @enonic-types/lib-react4xp @types/react

You probably also want some of these:

npm install --save-dev @enonic-types/lib-admin @enonic-types/lib-app @enonic-types/lib-auditlog @enonic-types/lib-auth @enonic-types/lib-cluster @enonic-types/lib-common @enonic-types/lib-content @enonic-types/lib-context @enonic-types/lib-event @enonic-types/lib-export @enonic-types/lib-grid @enonic-types/lib-i18n @enonic-types/lib-io @enonic-types/lib-mail @enonic-types/lib-node @enonic-types/lib-portal @enonic-types/lib-project @enonic-types/lib-repo @enonic-types/lib-scheduler @enonic-types/lib-schema @enonic-types/lib-task @enonic-types/lib-value @enonic-types/lib-vhost @enonic-types/lib-websocket

Then create or edit a tsconfig.json file:

tsconfig.json
{
    "compilerOptions": {
        "jsx": "react",
        "lib": [
            "DOM", // The server doesn't supports DOM, beeing permissive
            "ES2015",
        ],
        "moduleResolution": "node",
        "paths": {
          "/lib/enonic/react4xp": ["node_modules/@enonic-types/lib-react4xp"],
          "/lib/xp/*": ["node_modules/@enonic-types/lib-*"],
          "/*": ["src/main/resources/*"],
        },
        "skipLibCheck": true,
        "target": "ES2015",
        "typeRoots": [
          "node_modules/@types",
          "node_modules/@enonic-types"
        ]
    },
    "include": [
        "./src/main/resources/**/*.ts",
        "./src/main/resources/**/*.tsx"
    ],
}

TypeChecking for React4xp code (tsconfig.react4xp.json)

First install the typescript compiler, needed to run typecheking:

npm install --save-dev typescript

Then created a tsconfig.react4xp.json file:

tsconfig.react4xp.json
{
    "compilerOptions": {
        "jsx": "react",
        "lib": [
            "DOM",
            "ES2015",
        ],
        "moduleResolution": "node",
        "paths": {
          "/lib/enonic/react4xp": ["node_modules/@enonic-types/lib-react4xp"],
          "/*": ["src/main/resources/*"],
        },
        "skipLibCheck": true,
        "target": "ES2015",
        "typeRoots": [
          "node_modules/@types",
          "node_modules/@enonic-types"
        ]
    },
    "include": [
        "./src/main/resources/**/*.tsx"
    ],
}

And finally add a script to your package.json file:

package.json
  "scripts": {
    "verify:types:react4xp": "npx tsc --noEmit -p tsconfig.react4xp.json"
  }

You can now check the types of your React4xp files with this command:

npm run verify:types:react4xp

React4xp components

You may want to convert your React components from EcmaScript to TypeScript.

git mv Component.jsx Component.tsx

On mac this should rename all jsx files under src/main/resources

for filePath in $(find src/main/resources -iname "*.jsx"); do git mv $filePath "$(echo $filePath | rev | cut -d '.' -f 2- | rev).tsx"; done

Start adding types for parameters, etc.


Contents

Contents