Recommended upgrade
Contents
Step by step
This describes upgrading to the full feature set of react4xp 6 including migrating pages and layouts to react.
You will now have one application with your schemas, and one containing your frontend. If you prefer having a single application, follow these steps:
-
Move the cms schemas and other files you want to keep to the new application.
-
Rename it to the same name as your old application.
-
Make a production build and Deploy it.
-
The old application can now be discarded.
Migrating parts
Now let’s go a little bit deeper and use the example component as a part along with our new components. To do this we need to do the following:
-
Make an xml file for it so XP knows that it exists, here we can also give it a good description.
src/main/resources/site/parts/example/example.xml<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <part> <display-name>Example part</display-name> <description>Example part description</description> </part>
-
Move the example component to the
parts/example
folder and update as follows.src/main/resources/react4xp/components/parts/example/example.tsximport Hello from '/lib/myReactComponents/Hello'; import dayjs from 'dayjs'; import React, {useState} from 'react'; import type {ComponentProps, PartData} from '@enonic/react-components'; export const Example = (props: ComponentProps<PartData>) => { const [count, setCount] = useState(0); return <> <Hello/> <div>Part: {dayjs().format()}</div> <button onClick={() => setCount(prev => prev + 1)}>{count}</button> </>; }; export default Example;
Notice how the Example component receives
ComponentProps<PartData>
type props. This is going to be true for any other component as well, except thatPartData
, specifying type of the component field, will beLayoutData
for layouts, orPageData
for pages:interface ComponentProps<T extends ComponentData = ComponentData> { meta: MetaData; (1) component: T; (2) data?: Record<string, unknown>; (3) common?: Record<string, unknown>; (4) }
1 | Runtime metadata, such as the current content path, id and type as well as request mode. |
2 | Enonic XP component data that contains type, descriptor and some extra data based on the type (i.e. regions for layouts and pages). |
3 | Component data fetched by the associated data processor, if any. |
4 | Common data fetched by the common processor, if any.
|
Pages
Your current page part might look something like this:
import {Regions} from '@enonic/react-components';
import dayjs from 'dayjs';
import React from 'react';
import type {PageComponentProps} from './default.d';
function Page(props: PageComponentProps) {
return (
<div className="default-page">
<Regions {...props} />
<div>Page: {dayjs().format()}</div>
</div>
);
}
export default (props: PageComponentProps) => <Page {...props}/>;
with a controller like this:
import {render} from '/lib/enonic/react4xp';
import {getContent} from '/lib/xp/portal';
import type {Enonic} from '@enonic/js-utils/types/Request';
import type {PageComponentProps} from './default.d';
export function get(request: Enonic.Xp.Http.Request) {
const content = getContent();
const {page: entry} = content;
const react4xpId = `react4xp_${content._id}`;
const props: PageComponentProps = {
regionsData: content.page.regions,
names: "main",
tag: "main",
};
const htmlBody = `<!DOCTYPE html><html lang="en">
<head>
<meta charset="UTF-8">
<title>${content.displayName}</title>
</head>
<body class="xp-page">
<div id="${react4xpId}"></div>
</body>
</html>`;
const output = render(
entry,
props,
request,
{
body: htmlBody,
hydrate: false,
id: react4xpId,
}
);
return output;
}
And an xml like this:
<page>
<display-name>Default page</display-name>
<description>R4xp5 react-rendered page controller</description>
<form/>
<regions>
<region name="main"/>
</regions>
</page>
This is how we migrate it to a react4xp v6 component:
First let’s handle the controller which becomes a processor, the most important prop to pass from the processor used to be the regionsData. We can still get regions from component and return it, but this is not necessary anymore!
Component gets passed to the view automatically now, so you don’t have to define a processor function if it’s only regions you want to access. |
import {PageComponent} from "@enonic-types/core";
import type {ComponentProcessor} from '@enonic-types/lib-react4xp/DataFetcher';
export const pageProcessor: ComponentProcessor<'com.enonic.app.react4xp:Page'> = (props) => {
const {regions} = props.component as PageComponent;
return {
regions,
name: "main"
};
};
Next we need to change out the part default.tsx
with a component Page.tsx
, which is very similar except that we pass component
to the Regions
react component, because it contains regions now.
import {Regions, type ComponentProps, type PageData} from '@enonic/react-components';
import dayjs from 'dayjs';
import styles from './Page.module.css';
export const Page = ({data, common, meta, component}: ComponentProps<PageData>) => {
return (
<div className={styles[data.name as string]}>
<Regions common={common} meta={meta} component={component}/>
<div>Page: {dayjs().format()}</div>
</div>
);
};
In this example there is also a css module that is used to style the page.
.main {
padding-bottom: 0;
margin-bottom: 0;
display: flow-root;
}
For XP to see it we need to keep the xml file, but we will change the name to Page.xml
and add a description to it. This way we can call on it using a descriptor in the component registry and data fetcher.
<page>
<display-name>Upgraded page</display-name>
<description>Upgraded page component</description>
<form/>
<regions>
<region name="main"/>
</regions>
</page>
Let’s add it to componentRegistry and dataFetcher, this is done in the same way as we did with the part.
import {ComponentRegistry} from '@enonic/react-components';
import {Page} from './components/page/Page';
...
export const componentRegistry = new ComponentRegistry();
...
componentRegistry.addPage('com.enonic.app.react4xp:Page', {View: Page});
import {DataFetcher} from '/lib/enonic/react4xp';
import {pageProcessor} from './components/page/PageProcessor';
...
export const dataFetcher = new DataFetcher();
...
dataFetcher.addPage('com.enonic.app.react4xp:Page', {processor: pageProcessor});
Now we have a fully functional page component that is using the new regions and component registry.
Layouts
Lastly we need to migrate the layout, this is done in a similar way to the page.
Your current layout part might look something like this:
import {Regions} from '@enonic/react-components';
import dayjs from 'dayjs';
import React from 'react';
export default (props: Parameters<typeof Regions>[0]) => {
return <div style={{
columnGap: '1em',
display: 'grid',
gridTemplateColumns: '1fr 1fr'
}}>
<Regions {...props}/>
<div>{dayjs().format()}</div>
</div>;
};
with a controller like this:
import {render} from '/lib/enonic/react4xp';
import {getComponent} from '/lib/xp/portal';
import type {Enonic} from '@enonic/js-utils/types/Request';
import type {Regions} from '@enonic/react-components';
export function get(request: Enonic.Xp.Http.Request) {
const component = getComponent();
const props: Parameters<typeof Regions>[0] = {
classes: true,
names: ['left', 'right'],
regionsData: component.regions,
tags: 'section',
};
return render(
component,
props,
request,
{
hydrate: false,
}
);
}
And an xml like this:
<layout>
<display-name>Two columns</display-name>
<description>Two columns react-rendered layout controller</description>
<form/>
<regions>
<region name="left"/>
<region name="right"/>
</regions>
</layout>
Similar to the page, we need to implement a processor, and a component.
import {LayoutComponent} from '@enonic-types/core';
import type {ComponentProcessor} from '@enonic-types/lib-react4xp/DataFetcher';
export const layoutProcessor: ComponentProcessor<'com.enonic.app.react4xp:TwoColumns'> = ({component}) => {
const {regions} = component as LayoutComponent;
return {
regions,
tags: 'section'
};
};
The Layout component is depending on regions like the page, but let’s use Region
built-in component this time to render every region manually. We need to pass list of components as data to each Region
component:
import {type ComponentProps, type LayoutData, Region} from '@enonic/react-components';
import dayjs from 'dayjs';
import styles from './TwoColumn.module.css';
export const TwoColumnLayout = ({component, meta, data}: ComponentProps<LayoutData>) => {
return <>
<div className={styles.row + ' ' + styles[data.tags as string]}>
<Region data={component.regions.left.components} meta={meta} name="left"/>
<Region data={component.regions.right.components} meta={meta} name="right"/>
</div>
<div>Layout:{dayjs().format()}</div>
</>;
};
Notice how we used data fetcher result stored in data field to access the tags property. |
.section {
display: flex;
gap: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.section :global(> div) {
flex: 1;
min-width: 200px;
}
.section :global(.xp-page-editor-region-placeholder) {
margin-bottom: 0;
}
We need to add the layout to the component registry and data fetcher, this is done in the same way as we did with the part and page.
import {ComponentRegistry} from '@enonic/react-components';
import {TwoColumnLayout} from './components/layouts/TwoColumn';
...
export const componentRegistry = new ComponentRegistry();
...
componentRegistry.addLayout('com.enonic.app.react4xp:TwoColumns', {View: TwoColumnLayout});
import {DataFetcher} from '/lib/enonic/react4xp';
import {layoutProcessor} from './components/layouts/TwoColumnProcessor';
...
export const dataFetcher = new DataFetcher();
...
dataFetcher.addLayout('com.enonic.app.react4xp:TwoColumns', {processor: layoutProcessor});
And finally we need to update the xml file to match the descriptor, this is done in the same way as the page.
<layout>
<display-name>Two columns</display-name>
<description>Upgraded Two columns controller</description>
<form/>
<regions>
<region name="left"/>
<region name="right"/>
</regions>
</layout>
Now we have a fully functional layout component that is using the new regions and component registry.
And just like that we have not only upgraded the old component, but also made it compatible with the new ones!