Hello React - server and clientside rendering

Contents

In this chapter you will be introduced to how XP controllers may use TSX/JSX templates for both server-side and client-side rendering

Basic usage

The following files are involved in this example, each file will be described in details below

/src/main/resources/
 controllers/hello-react.ts
 react4xp/entries/hello-react.tsx
 site/site.xml

React template

Start off by adding the following React component (tsx) file to your project:

/src/main/resources/react4xp/entries/hello-react.tsx
import React from 'react';

function HelloReact({displayName}) { (1)
  let bottleCount = 99;
  function dropBottle() {
    bottleCount--;
    console.log(bottleCount, 'bottles of beer on the wall.');
    (document.getElementById('counter') as HTMLElement).innerText = bottleCount.toString();
  };

  return (
    <div onClick={dropBottle}>
      <h1>Hello {displayName}!</h1>
      <button><span id="counter">{bottleCount}</span> bottles of beer</button>
    </div>
  );
}

export default (props) => <HelloReact {...props}/>;  (2)
1 React component that will render a heading, and a button that will count down when clicked.
2 The export default line is required, it may take a props object and must return a react component. More on this later

This component will be rendered server-side, and then hydrated so it will also work on the client-side, for an amusing beer bottle countdown. The actual updating step is done with vanilla JS instead of actual react, just to keep things as simple as possible. We’ll look at stateful/dynamic components later.

XP controller

To trigger the rendering, we’ll use a regular Enonic controller (ts). Add the following file to your app:

/src/main/resources/controllers/hello-react.ts
import {render} from '/lib/enonic/react4xp';
import {getContent} from '/lib/xp/portal';

export function get(request) {               (1)
  const {displayName} = getContent(); 
  const props = {displayName};
  const react4xpId = 'react4xpApp';          (2)
  const component = 'hello-react';           (3)

  return render(                             (4)
    component,
    props,
    request,
    {
      id: react4xpId,                        
      body:                                  (5)
        `<!DOCTYPE html><html lang="en">
        <head>
            <meta charset="UTF-8">
        </head>
        <body class="xp-page">
            <div id="${react4xpId}"></div>
        </body>
        </html>`,
      ssr: true,                             (6)
      hydrate: true,                         (7)
    }
  );
}
1 Standard XP controller implemeting the HTTP get function.
2 ID for the react app (TODO)
3 React templates placed in src/resources/main/react4xp/entries may simply be referenced by their name, using a concept we call jsxPath).
4 The output of React4XP render() is a regular XP response object.
5 Bootstrap markup for the React application
6 ssr true means the component will be Server Side Rendered
7 hydrate true means the component will be activated on client-side

Controller mapping

To trigger the controller, we’ll add a simple mapping to site.xml - this will cause the controller to execute whenever someone previews a site where the application is deployed:

/src/main/resources/site/site.xml
<?xml version="1.0" encoding="UTF-8"?>
<site>
  <form/>
  <mappings>

    <mapping controller="/controllers/hello-react.js">
      <match>type:'portal:site'</match>
    </mapping>

    <!-- Added during the guillotine tutorial -->
    <mapping controller="/controllers/previewMovie.js" order="50">
      <match>type:'com.enonic.app.samples_react4xp:movie'</match>
    </mapping>

    <!-- Also added during the guillotine tutorial -->
    <mapping controller="/headless/guillotineApi.js" order="50">
      <pattern>/api/headless</pattern>
    </mapping>

  </mappings>
</site>

SSR vs client side gotchas

When using isomorphic JavaScript, some functionality may only be working on server-side, and some only on client side, like document and window - which are pure client-side concepts. React4xp has polyfilled window as empty object. To prevent weird behavior or errors, take care to only execute browser-specific code in the browser.

In the example above, dropBottle is the response to a click listener which won’t be triggered to actually run on the server (although the asset is compiled and read the asset into memory, but that is safe in itself), so we don’t need any more safeguards here. But if you do, it’s easy enough - for example:

if (typeof window.navigator !== 'undefined') {
    // ...This won't run during SSR...
}

Of course, this also applies to imported packages / nested components and so on. Importing them is usually safe, but consider wrapping the areas where they’re called/used.

Preview

  1. Your changes should automatically be picked up by XP, to manually deploy use (enonic project deploy in a terminal from your project root)

  2. Visit localhost:8080/admin. Log in to XP and open Content Studio.

  3. Create a new content project, then create a new Site.

  4. Add your React4xp app ("starter-react4xp"?) to the site

    hello cs

    After saving, you should now see something like this:

    hello cs

    In order to support a visual editing experience, components will not be hydrated.

  5. Click Preview for a live view of the page.

    hello bottles

    Clicking anywhere on the text should trigger the dropBottle function from hello-react.tsx, modify the DOM and output a message in the browser console. Look at those bottles go!

You might get serverside runtime errors in the react components that you write. The React4xp rendering engine will dump an error message and some suspected code in the server log - but it’s not always easy to make sense of those.

Switching over to clientside rendering (SSR false, Hydrate true) for the particular react component will often give you a better/sourcemapped error message in the browser console, making your debugging life easier.

The output

Back to the actual rendering of HTML. Looking at the page source code, you should see the following:

INFO: 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 Sitename</h1>
            <p><span id="counter">99</span> bottles of beer on the wall.</p>
        </div>
    </div>

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

We can see this whole output is actually the body HTML string we passed into the render call in the controller - but a lot has been inserted. Most importantly, three assets are loaded into the client.

1 globals.<contenthash>.js is react and react-dom bundled together. For better security, they are served from XP instead of a random CDN.
2 runtime.js containst chunked boilerplate code to run entries.
3 hello-react 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 The script that actually runs hydrate with the props on the clientside.
Most of these static assets are content-hashed, and optimized for client-side caching - thus running a CDN in front of XP will speed up the delivery of these.

So, we are rolling, next up lets look into using pages and editorial components.


Contents

Contents