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 happes because the application declares a "catch all" pattern mapping in site.xml
.
<?xml version="1.0" encoding="UTF-8"?>
<site>
<form/>
<mappings>
<mapping controller="/site/app.js" order="10"> (1)
<pattern>/.*</pattern>
</mapping>
<mapping controller="/site/component.js" order="10"> (2)
<service>component</service>
</mapping>
</mappings>
</site>
1 | Pattern mapping to catch all requests |
2 | Enables drag’n drop support in the visual page editor |
Controller
The controller is by default located /src/main/resources/site/controllers/app.ts
. It uses a traditional MVC approach:
-
Get component props using the dataFetcher
const content = getContent(); const { componentProps, response } = dataFetcher.execute({ content, request, });
-
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>`;
-
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.
import type { AppProps } from '/types/AppProps';
import * as React from 'react';
import {BaseComponent} from '@enonic/react-components';
import {componentRegistry} from '../componentRegistry';
const App: React.FC<AppProps> = (props) => {
return (
<BaseComponent componentRegistry={componentRegistry} {...props}/>
);
}
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/hello-react.js"></script> (3)
<script defer src=".../react4xp/client-QZN5J5VC.global.js"></script> (4)
<script data-react4xp-app-name="com.enonic.app.samples_react4xp" data-react4xp-ref="react4xpApp" type="application/json">{"command":"hydrate","devMode":false,"hasRegions":0,"isPage":1,"jsxPath":"site/pages/hello-react/hello-react","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 | hello-react.js is the compiled version of hello-react.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. |