Hello React - server and clientside rendering

Contents

Lesson overview

React4xp is made to cover many ways of combining XP and react. Let’s start with a simple scenario: displaying a working react component in content studio.

We’ll first do that in the serverside rendered way (this is the react4xp default). And afterwards add an option flag that turns the rendering into clientside-rendering instead.


Code

Lets start by setting up a regular XP page controller called hello-react.

We’ll add the following files to the project
/src/resources/site/pages/hello-react/
  hello-react.xml
  hello-react.jsx
  hello-react.es6

Page descriptor

First, we need a *page * XML file. Just XP boilerplate:

pages/hello-react/hello-react.xml:
<page>
  <display-name>Hello React</display-name>
  <description>Super simple example</description>
  <form />
</page>


React component

Next, we’ll add the react component. This displays a "Hello world" heading, and we’ll add some simple functionality to it: every time you click the message, it will update a number in the next line and print a message in the console. This is to show that we’re rendering an active react component; we’re not only using JSX as an XP templating language for static HTML - although sure, you could do that too if you want.

Obviously, the actual updating step is done with vanilla JS instead of actual react, just to keep everything as simple as possible. We’ll look at stateful/dynamic components later.

hello-react.jsx
import React from 'react';

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

export default (props) => <div onClick={dropBottle}>                (1)
    <h1>Hello world!</h1>
    <p>Click me: <span id="counter">{bottleCount}</span> bottles of beer on the wall.</p>
</div>;
1 One thing is mandatory for this to work, shown in the export default line: the JSX file must default-export a function that may take a props object and must return a react component. More on this later.

Apart from that, you can use ordinary ES6 and import and nest other JS, react components and third-party stuff from node_modules/ in the regular way. We’ll also get back to that later, including a caveat or two.

By the way, a quick gotcha on using document and window etc, during serverside vs clientside rendering:

Naturally, a lot of browser-specific functionality won’t work well during server-side rendering. React4xp has polyfilled both document and window as empty objects in Nashorn. To prevent weird behavior or errors, browser-specific code should only run in the browser.

In this example, 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 nashorn, 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, like this.


Page controller

Finally, we’ll add a bare-bone page controller that calls the rendering engine:

hello-react.es6:
const portal = require('/lib/xp/portal');
const React4xp = require('/lib/enonic/react4xp');       (1)

exports.get = function(request) {                       (2)
    const entry = portal.getComponent();                (3)
    const props = null;

    return React4xp.render(                             (4)
        entry,
        props,
        request,
        {
            id: "react4xpApp",                          (5)
            body:                                       (6)
                `
                    <html>
                        <head></head>
                        <body class="xp-page">
                            <div id="react4xpApp"></div>
                        </body>
                    </html>
                `
        }
    )
};

That’s it.

If you’ve used XP before, you’ll probably note that there’s no HTML view file, no thymeleaf.render, and so on - in this example, react4xp completely handles the page view. The essential thing here is that at the end of the get function, react4xp in a single .render call creates a full XP response object that makes react work.

What’s going on in the controller?

1 In line 2, React4xp is imported from the library.
2 In line 4, we pick up the request data. The render call needs it to know the rendering context.
3 In line 5, we get the XP component data. Used directly in the .render call, it’s a convenient shortcut for react4xp to "this XP component", for finding the same-name react component in the same folder: hello-react.jsx. React4xp has more ways to refer to react components, or entries (we’ll get to that, but for all the juicy details: entries and jsxPath).
4 In line 8, the juicy bit: the actual render call. The rendered response from it can be returned directly from the get function, since .render creates an object with a body HTML string and a pageContributions attribute with everything needed (more details: the render API).
5 In line 13, the id attribute "react4xpApp" is set in the options argument of .render (lines 12-20). This is the unique ID we’re giving to the react component, and the HTML id of the target element where react will render the component in the end. Note that the same ID is found in an element in the HTML body:
6 In lines 14-19, we’re hardcoding a body option, an HTML string as a base for the output, with a "react4xpApp" ID element. In many cases you can do without this string but here it’s needed for the output, since react itself doesn’t like to render the tags <html>, <head>, <body> or anything outside of a containing root tag (such as <!DOCTYPE html>).


Setup and first render

If you’ve added those 3 files, let’s get this rendered! If you’ve used XP and Content Studio before, this is all run-of-the mill:

  1. Compile the project (enonic project deploy in a terminal from root) and start the sandbox (enonic sandbox start),

  2. Navigate your browser to localhost:8080/admin. Log in to XP and open Content Studio (if you haven’t already, you’ll need to install Content Studio in XP).

  3. Create a new Site content and Edit it in a new tab. Add your react4xp app ("starter-react4xp"?) on the upper left.

  4. Select the new Hello-react page controller in the preview panel on the right. Store that change (and refresh the page if needed).

You should now see something like this:

hello cs


  1. Click Preview on the top to open a new tab and view the content outside of Content Studio:

hello bottles


Clicking somewhere on the rendered text in the preview window will trigger the dropBottle function from hello-react.jsx, and modify the DOM and output a message in the browser console. Look at those bottles go!


About the rendering

Two things are worth knowing about the rendering, before we move on:


Content studio should only have static rendering

If you clicked the rendering inside XP Content Studio instead of a preview tab, you’d notice there was no bottle-counting. This is on purpose: react functionality may intervene with the Content Studio editorial workflow, or even disrupt Content Studio itself. Therefore, the request argument is used in React4xp.render to handle this automatically: inside Content Studio, you’ll only see the rendering as a regular static XP preview / placeholder instead of active react.

Later, we’ll look at two other rendering functions: .renderBody and .renderPageContributions. These are intended for use cases where it’s good to be more explicit than React4xp.render. For that reason, they don’t automatically handle this for you. Usually, you should still keep your components from being client-activated inside Content Studio, but you’ll have to handle it yourself. The custom flow chapter shows you how.


First serverside render can be slow

You may also have noticed that it took a few seconds for the very first rendering to be displayed, either here or in the edit/browse mode in Content Studio. That’s the Nashorn server-side rendering engine warming up. It reads and caches the basics (notably, some necessary polyfilling, react and react-dom) for performance.

We’re looking into mitigating this delay in the future, but for now, this is nice to know: This delay only happens when your app is restarted, i.e. you restart XP entirely, or redeploy the app. Setting up a continuous build with XP devmode instead, helps. For the react4xp starter, here’s how:

  1. enonic sandbox start <yourSandBoxName> --dev in one terminal (from anywhere), and

  2. enonic project deploy && gradlew react4xp_components compileXP -t from project root in another terminal. Reply No if it asks you to start the sandbox here.


Output

Okay, back to the rendering of the page. Open the page source code in the browser. Here’s what React4xp.render created - the response the client receives on the initial page request:

<html>
<head></head>
<body class="xp-page">

    <div id="react4xpApp">
        <div data-reactroot="">                                                                             (1)
            <h1>Hello world!</h1>
            <p>Click me: <span id="counter">99</span> bottles of beer on the wall. </p>
        </div>
    </div>

    <script src="(...your.app.service) /react4xp/externals.489d97cdf.js"></script>                          (2)
    <script src="(...your.app.service) /react4xp/client.5678abcd.js"></script>                              (3)
    <script src="(...your.app.service) /react4xp/site/pages/hello-react/hello-react.12345678.js"></script>  (4)
    <script src="(...your.app.service) /react4xp/dynamic.87654321.js"></script>                             (5)
</body>
</html>

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

(The asset URLs are shortened for readability, and because some details may vary. At my computer for example, the (…​your.app.service) part actually looks like: /admin/site/preview/default/draft/hello-react/_/service/com.enonic.app.react4xp/)

1 At the top, we see the <div id="react4xpApp"> target container, now filled with a server-side rendering of the react component. At this point it’s only static markup, but it will be activated during step 5 below.
2 The first asset is externals.<contenthash>.js: this is react and react-dom bundled together. They are served from XP instead of from a CDN.
3 The second loaded asset is a client-wrapper.
4 The third asset is the compiled version of hello-react.jsx, with the react component and the dropBottle routine. During React4xp.render, react4xp used the component data to locate this asset after compiling. The react component gets an identifier string, site/pages/hello-react/hello-react, which is called a jsxPath in react4xp. We’ll cover jsxPaths later (full detail reference here), but for now you just need to know that this identifier is also used when loading this asset into the the browser’s namespace: React4xp['site/pages/hello-react/hello-react'].
5 The script that actually runs hydrate with the props on the clientside.
The assets are served by lib-react4xp services. Most of them (react4xp-client and the content-hashed assets) are optimized for client-side caching, to minimize repeated requests.


Client-side rendering

Sometimes you might want or need to skip the server-side rendering of a react component, and relay the react rendering entirely to the browser. This a one-line operation in React4xp.render.

Let’s return to the controller and add a line on line 23:

hello-react.es6:
const portal = require('/lib/xp/portal');
const React4xp = require('/lib/enonic/react4xp');

exports.get = function(request) {
    const entry = portal.getComponent();
    const props = null;

    return React4xp.render(
        entry,
        props,
        request,
        {
            id: "react4xpApp",
            body:
                `
                    <html>
                        <head></head>
                        <body class="xp-page">
                            <div id="react4xpApp"></div>
                        </body>
                    </html>
                `,
            clientRender: true                          (1)
        }
    )
};
1 On the server, a truthy clientRender flag in the options object makes the server skip the HTML rendering (in this particular .render call. Mixing up clientside and serverside rendering across different places hasn’t been tested very much, but it should work fine).

If you compare with the serverside-rendered example, the clientRender flag causes the rendered output to change slightly, changing the behavior in the browser.

<html>
<head></head>
<body class="xp-page">

    <div id="react4xpApp"></div>    (1)

    (2)
    <script src="(...your.app.service) /react4xp/externals.489d97cdf.js"></script>
    <script src="(...your.app.service) /react4xp/client.5678abcd.js"></script>
    <script src="(...your.app.service) /react4xp/site/pages/hello-react/hello-react.12345678.js"></script>

    <script src="(...your.app.service) /react4xp/dynamic.87654321.js"></script>                            (3)
</body>
</html>
1 As expected, the target container is no longer filled with a serverside-rendered HTML representation of the react component. Instead, the browser fills in the DOM from scratch in step 3 below.
2 The compiled assets and their URLs are exactly the same as in the serverside version.
3 The script that actually runs render with the props on the clientside.
As mentioned before, this only applies outside of Content Studio. The clientRender flag does not change anything inside Content Studio: still server-side rendered static HTML.

Apart from these differences behind the scenes, the page will look and behave the same when presented to the user.

Finally, it’s worth mentioning a special case where you might want to temporarily clientside-render a component:

You might get serverside runtime errors in the react components that you write. The Nashorn 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.

Often, switching over to clientside rendering for that particular react component, will give you a better/sourcemapped error message in the browser console, making your debugging life easier.


Okay, ready for the next example lesson?




Contents

Contents