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.
Read more under Application configuration and React4xp components
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:
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.
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:
react4xp.serveGlobals = false
react4xp.ssr
Add this to your application configuration file:
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.
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
})
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: https://legacy.reactjs.org/docs/error-decoder.html/?invariant=321 https://react.dev/warnings/invalid-hook-call-warning#duplicate-react Luckily there is a workaround: You can use Application Configuration to disable the serving of the globals bundle.
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
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:
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:
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:
{
"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
// 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
import {render} from '/lib/enonic/react4xp';
export function get(request) {
return render(component, props, request, {
// Optional
// hydrate: false,
// ssr: false
});
}
TypeScript class
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
const libReact4xp = require('/lib/enonic/react4xp');
exports.get = function (request) {
return libReact4xp.render(component, props, request, {
// Optional
// hydrate: false,
// ssr: false
});
}
Common.JS class
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:
{
"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:
{
"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:
"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.