Recommended upgrade

Contents

Step by step

This describes upgrading to the full feature set of react4xp 6 including migrating pages and layouts to react.

  1. Create an app using the starter and give it a new name.

  2. Migrate each react component to the new app.

  3. Migrate your page and layout to react components

  4. Deploy the application on your existing site.

You will now have one application with your schemas, and one containing your frontend. If you prefer having a single application, follow these steps:

  1. Move the cms schemas and other files you want to keep to the new application.

  2. Rename it to the same name as your old application.

  3. Make a production build and Deploy it.

  4. 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:

  1. 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>
  2. Move the example component to the parts folder, where we update it by wrapping it in a Part tag instead of div. Then we add extraProps to it, this is important because react4xp v6 uses this to add necessary props to the parts component so it is possible to view and edit it within the page editor.

    src/main/resources/react4xp/components/parts/example/example.tsx
    import Hello from '/lib/myReactComponents/Hello';
    import {Part} from '@enonic/react-components';
    import dayjs from 'dayjs';
    import React, {useState} from 'react';
    
    export const Example = (props: any) => {
    
        const {componentRegistry, ...extraProps} = props;
        const [count, setCount] = useState(0);
    
        return (
            <Part {...extraProps}>
                <Hello/>
                <div>Part: {dayjs().format()}</div>
                <button onClick={() => setCount(prev => prev + 1)}>{count}</button>
            </Part>
        );
    };
    
    
    export default Example;
  3. Instead of fetching in get(), create a processor that returns its props—React4xp will forward them (plus its own metadata) into <Part> via extraProps.

    src/main/resources/react4xp/components/parts/example/exampleProcessor.ts
    import type {ComponentProcessorFunction} from '@enonic-types/lib-react4xp/DataFetcher';
    
    export const partProcessor: ComponentProcessorFunction<'com.enonic.app.react4xp:example'> = (props) => {
        return {};
    };
  4. Add it to the componentRegistry and dataFetcher with the same descriptor as in previous task.

    src/main/resources/react4xp/componentRegistry.ts
    import {ComponentRegistry} from '@enonic/react-components';
    import Example from './components/parts/example/example';
    
    ...
    
    export const componentRegistry = new ComponentRegistry();
    
    ...
    
    componentRegistry.addPart('com.enonic.app.react4xp:example', {View: Example});
    src/main/resources/react4xp/dataFetcher.ts
    import {DataFetcher} from '/lib/enonic/react4xp';
    import {partProcessor} from './components/parts/example/exampleProcessor';
    
    ...
    
    export const dataFetcher = new DataFetcher();
    
    ...
    
    dataFetcher.addPart('com.enonic.app.react4xp:example', {processor: partProcessor});

Pages

Your current page part might look something like this:

src/main/resources/site/pages/default/default.tsx
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:

src/main/resources/site/pages/default/default.ts
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:

src/main/resources/site/pages/default/default.xml
<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 is the regionsData. Notice how we change regionsData to regions, this is because the new Regions React component is expecting it along with componentRegistry.

src/main/resources/react4xp/components/page/PageProcessor.ts
import {PageComponent} from "@enonic-types/core";
import type {ComponentProcessorFunction} from '@enonic-types/lib-react4xp/DataFetcher';

export const pageProcessor: ComponentProcessorFunction<'com.enonic.app.react4xp:Page'> = (props) => {
    const component = props.component as PageComponent;

    const regions = component?.regions || {};
    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 regions to the Regions react component.

src/main/resources/react4xp/components/page/Page.tsx
import {Regions,} from '@enonic/react-components';
import dayjs from 'dayjs';
import React from 'react'
import styles from './Page.module.css';

export const Page = (props: any) => {


    return (
        <div className={styles[props.name]}>
            <Regions {...props} />
            <div>Page: {dayjs().format()}</div>
        </div>
    );
};

In this example there is also a css module that is used to style the page.

src/main/resources/react4xp/components/page/Page.module.css
.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.

src/main/resources/site/pages/Page/Page.xml
<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.

src/main/resources/react4xp/componentRegistry.ts
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});
src/main/resources/react4xp/dataFetcher.ts
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: .src/main/resources/site/layouts/twoColumns/twoColumns.tsx

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: .src/main/resources/site/layouts/twoColumns/twoColumns.ts

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:

src/main/resources/site/layouts/twoColumns/twoColumns.xml
<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.

src/main/resources/react4xp/components/layouts/TwoColumnProcessor.ts
import {LayoutComponent} from '@enonic-types/core';
import type {ComponentProcessorFunction} from '@enonic-types/lib-react4xp/DataFetcher';


export const layoutProcessor: ComponentProcessorFunction<'com.enonic.app.react4xp:TwoColumns'> = ({component}) => {
    const {regions} = component as LayoutComponent;

    return {
        regions: regions,
        tags: 'section'
    };
};

The Layout component is depending on the Regions like the page, so we need to pass the regions prop to it.

src/main/resources/react4xp/components/layouts/TwoColumn.tsx
import {Layout} from '@enonic/react-components';
import dayjs from 'dayjs';
import React from 'react'
import styles from './TwoColumn.module.css';


export const TwoColumnLayout = (props: any,) => {

    return (
        <>
            <Layout className={styles[props.tags]} {...props}/>
            <div>Layout:{dayjs().format()}</div>
        </>
    );
};

We are keeping one more prop for layout, we use this tags: 'section' to target the layout with our css module.

src/main/resources/components/layouts/twoColumn.css
.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.

src/main/resources/react4xp/componentRegistry.ts
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});
src/main/resources/react4xp/dataFetcher.ts
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.

src/main/resources/site/layouts/TwoColumn/TwoColumn.xml
<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.

(Migrate thymeleaf etc, minimal example) 0

And just like that we have not only upgraded the old component, but also made it compatible with the new ones!


Contents

Contents

AI-powered search

Juke AI