App controller

Contents

The app controller is responsible for bootstrapping all important elements of React4XP.

Site mapping

React4XP runs on top of XP’s site engine. By adding a React4XP application to a site, it is injected into request pipeline - where it hijackes the rendering.

This happens because the application declares a "catch all" pattern mapping in site.yaml.

/src/main/resources/cms/site.yaml
kind: "Site"
mappings:
  - controller: "/cms/app.js" (1)
    order: 10
    pattern: "/.*"
  - controller: "/cms/component.js" (2)
    service: "component"
    order: 10
apis: (3)
  - "asset"
  - "react4xp"
1 Pattern mapping that catches all requests except the react4xp routes (invertPattern: true)
2 Enables drag’n drop support in the visual page editor
3 Mounts APIs to site

On Enonic XP 8, mounting the react4xp API is required wherever React4XP renders - see Mount the API.

Controller

The controller is by default located /src/main/resources/cms/app.ts. It uses a traditional MVC approach:

  1. Get component props using the dataFetcher

    const content = getContent();
    const {
      componentProps,
      response
    } = dataFetcher.execute({
      content,
      request,
    });
  2. Defines raw HTML with the React4XP app element

    const react4xpId = `react4xp_${content._id}`;
    const htmlBody = `<!DOCTYPE html><html lang="en">
    	<head>
    		<meta charset="UTF-8">
    		<title>${content.displayName}</title>
    	</head>
    	<body>
    		<div id="${react4xpId}"></div>
    	</body>
    </html>`;
  3. Invokes React4XP’s render function

    const output = render(
      'App', (1)
      props, (2)
      request,
      {
        body: htmlBody, (3)
        id: react4xpId, (4)
      }
    );
    
    return output;
1 React entry - React4XP will look for a matching component in /src/main/resources/react4xp/entries
2 Content from dataFetcher
3 The bootstrap HTML
4 Element ID to use for the React app

The render function kicks off React server-side rendering by invoking the App component.

/src/main/resources/react4xp/entries/App.tsx
import '@enonic/react-components/utils/initPublicPath';
import Footer from "/react4xp/components/common/Footer";
import '../components/globalStyles.css'
import type {MetaData} from '@enonic/react-components';
import {BaseComponent} from '@enonic/react-components';
import * as React from 'react';
import {componentRegistry} from '../componentRegistry';
import {AppProps} from '/types/AppProps';

const App: React.FC<AppProps> = ({component, data, common, meta}) => {
    const compMeta: MetaData = meta as MetaData;
    compMeta.componentRegistry = componentRegistry;
    return (
        <>
            <BaseComponent component={component} data={data} common={common} meta={compMeta}/>
            {
                (component.type == "page" || component.type == "contentType") && <Footer logoUrl={common.logoUrl as string}/>
            }
        </>
    );
}

App.displayName = 'App';

export default App;

From here on, it is all React.

Output

If you take a closer look at the page markup produced by the app controller, you should see something like this:

The links have been shortened for better readability
<!DOCTYPE html><html lang="en">
<head>
  <meta charset="UTF-8">
  <script defer src=".../react4xp/globals.js"></script>                            (1)
  <script defer src=".../react4xp/runtime.js"></script>                            (2)
  <script defer src=".../react4xp/App.js"></script>                                (3)
  <script defer src=".../react4xp/client-QZN5J5VC.global.js"></script>             (4)
  <script data-react4xp-app-name="my.first.r4xp" data-react4xp-ref="react4xpApp" type="application/json">{"command":"hydrate","devMode":false,"hasRegions":0,"isPage":1,"jsxPath":"App","props":{}}</script> (5)
</head>
<body class="xp-page">

    <div id="react4xpApp">
        <div data-reactroot="">                                                    (6)
            <h1>Hello React4XP</h1>
        </div>
    </div>

    <script defer src=".../react4xp/executor-BL4RRDZO.js"></script>                (7)
</body>
</html>

The result is the HTML string we passed to the render request of our initial controller - but a lot of stuff has also been inserted.

1 globals.<contenthash>.js is react and react-dom bundled together. For improved security, they are served from XP instead of a random CDN.
2 runtime.js containst chunked boilerplate code to run entries.
3 App.js is the compiled version of App.tsx
4 client-QZN5J5VC.global.js is the client-wrapper. TODO: What does it do?
5 Properties and props needed for the client-side rendering to work properly (pun intended)
6 The <div id="react4xpApp"> target container, now filled with a server-side rendering of the react component
7 Script that triggers hydration - more on this in the next chapter
Most of these static assets are content-hashed, and optimized for client-side caching - thus running a CDN in front of the application, in production, will speed up the delivery.

Contents

Contents