Server side JSX with React4XP

Contents

Server side JSX with React4XP

This tutorial will show you step-by-step how to build XP sites and apps, using React rendering.

This documentation expects basic knowledge of React, XP and Webpack.

React4xp logo

Introduction

This tutorial takes you through the basic steps of creating a React app, helping you kickstart a React4xp project of your own.

During this exercise you will:

  • set up the starter and meet the React4xp library,

  • learn how to use it to render a basic React component from any regular XP controller - in this case a part,

  • inject editorial data into your React component,

  • see how to point the controller to the React components - and where to put them in your project source,

  • dig a little deeper into ways of controlling the rendering,

Create project

Set up the starter project locally. With Enonic XP 7, run the following command:

enonic project create -r starter-react4xp

Remember to create a new XP sandbox when completing the project wizard.

Don’t have the Enonic CLI? Visit the Getting started guide to install it.

Project structure

If you’re used to working with a typical XP project using webpack, the structure will look familiar - the biggest news is the folder resources/react4xp/.

This structure is just an overview, and you don’t need to know it by heart to get started. For now, just put your JSX source files under react4xp/_entries/ or under site/, and you’re ready to build and run.

Your project folder should look something like this:

Selected files:
build.gradle (1)
settings.gradle (1)
package.json (2)
src/
  main/
    resources/
      react4xp/ (3)
        _entries/ (4)
          REPLACE_ME.jsx (5)
      site/
        pages/
            default/ (6)
              default.es6
              default.html
              default.xml
        parts/
          hello-react/ (7)
            hello-react.es6
            hello-react.jsx
            hello-react.xml
1 The gradle files are used to set up the build system: the lib-react4xp library and some building tasks,
2 package.json sets up NPM import of some packages that React4XP and the build process need,
3 Webpack will look for your React-specific JS and JSX source files under the react4xp/ folder.
4 You can add your own subfolder structure under /react4xp/, but note that react4xp/_entries/ is a reserved, "magic" name. This folder is where you put "entries" - the source files that will be accessible to use in the XP controllers as in the examples. It’s also possible to put entries (with .JSX-extension) along with XP components (e.g. in /site/parts or /pages).
5 Webpack needs something in its focus folders to avoid halting the build process, so REPLACE_ME.jsx is just an empty placeholder. You can remove it when you’ve added one or more React source files under here.
6 For convenience, a bare-bone page controller is included with the starter…​
7 …​along with the first example from this tutorial.

React4XP

This starter is based on React4XP, which is a library and accompanying build structures that facilitate use of React in XP. React4XP enables you to:

  • use JSX in server-side rendering, similar to other XP templating engines

  • supports isomorphic client- or serverside rendering and hydration,

  • use build flow and compilation with automated optimized asset serving

  • aims to be modular and tweakable, making it possible to pop the hood, pick it apart and set things up your own way. Godspeed!

  • is flexible enough to cover many more advanced use cases,

Build and Deploy

To build and deploy the starter app, run this command from your shell:

enonic project deploy

Accept starting the sandbox.

To verify that your app started successfully, you should find an entry similar to this in the sandbox log:

2019-04-09 13:40:40,765 INFO ... Application [<name.of.your.app>] installed successfully
For the examples below, you can copy/paste the code examples into your project, or download the examples along with everything needed to run, by using git and checking out the examples branch of starter-react4xp.

Hello React

Let’s make a simple XP part that displays "Hello World", using a react component: hello-react.

Files involved:
site/parts/hello-react/
  hello-react.xml
  hello-react.jsx
  hello-react.es6

Code

Add a part definition. It doesn’t need anything special, just XP boilerplate:

hello-react.xml:
<part>
  <display-name>Hello React</display-name>
  <description>Simple example with server-side rendering by default</description>
  <form />
</part>

Now for the React component itself:

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

export default (props) => <p>Hello {props.greetee}!</p>;

It takes a greetee prop and greets it with a booming "Hello"! This is our first example of an Entry:

Entries

Entries in React4XP are the React components that can be accessed by React4XP. They are just any standard JSX file, as long as it:

  1. default-exports a function that takes an optional props parameter and returns a React element,

  2. is placed either under the folder /react4xp/_entries, or in a part or page folder under site/.

When done, they are automatically handled by React4XP and can be easily used in XP controllers:

The part controller uses React4XP to render the entry:

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

exports.get = function(request) {
    const component = portal.getComponent();
    const props = { greetee: "world" };

    return React4xp.render(component, props, request);
};

It basically just imports React4xp and uses the .render function, similar to how you might be familiar with from Thymeleaf or other XP template engines:

  • The first argument is a reference to the template. In our case the template is the React4XP entry, and React4XP uses the component data (for the part itself) to locate the JSX file in the part’s own folder (expecting the same file name as the part).

  • The second argument, props, is similar to the model argument in the Thymeleaf renderer. No big surprise: it’s passed to the entry’s props. Here is the world we’re about to greet.

  • The third request argument is necessary for a fully activated React rendering. You can leave it out to render the entry as pure HTML if you like JSX as a pure templating language, but it won’t be activated (hydrated) in the browser.

The returned response object from .render is sent straight from the controller to the client, and contains:

  • a body field with a server-side rendering (in static HTML) of the entry with the entered props,

  • and some pageContributions that make the client activate the React entry (containing the necessary asset links and React hydration commands).

An important difference from Thymeleaf’s renderer is that React4XP.render generates a full response object that can be directly returned from the controller, instead of just HTML that you need to wrap in a body field in the controller’s response object.

That’s it. This part is ready to display in XP!

Setup

Let’s add it to a page in Content Studio and render it:

  • Run enonic project deploy from your shell, start the sandbox and point your browser to localhost:8080/admin.

  • Log in to XP and open Content Studio.

  • Create some content (e.g. a site with the Default Page controller included with the starter, or a a landing page). Anything with a Region will do.

  • Insert the new part into the region, and select the hello-react part you just made.

  • Enter the preview to view the content outside of Content Studio. You should now see:

Rendering: Hello World. In fabulous Times New Roman, because a good tutorial has no irrelevant layers of complexity.

Output

Curious about what happened here? View the page source code in the browser to see what .render created - something like this (the number-tagged lines):

<!DOCTYPE html>
<html>
  <head>(...)</head>

  <body>

    <main data-portal-region="main" class="xp-region">
      <div (...) id="parts_hello-react__main_0">
      	<p data-reactroot="">Hello <!-- -->world<!-- -->!</p> (1)
      </div>
    </main>

    <script src="(...) /react4xp/externals.88e80cab5.js"></script>  (2)
    <script src="(...) /react4xp-client/"></script> (3)
    <script src="(...) /react4xp/site/parts/hello-react/hello-react.js"></script> (4)
    <script defer> (5)
      React4xp.CLIENT.hydrate(
        React4xp['site/parts/hello-react/hello-react'],
        "parts_hello-react__main_0",
        { "greetee": "world", "react4xpId": "parts_hello-react__main_0" }
      );
    </script>

   </body>
</html>
1 A container element with an ID, and inside it: an HTML representation of the JSX entry, pre-rendered by React4XP on the server with the initial props.
2 A standard externals chunk (the exact path may vary with local setups, so it’s truncated to (…​). The same goes for the cache-busting hash in the filename). This contains React and ReactDOM, built-in with React4XP.
3 The React4xp client wrapper, which enables the hydration command in point #5, among other things. A global client-side object is created, React4xp, which will contain all things React4xp in runtime. The client wrapper is React4xp.CLIENT.
4 The entry itself - the compiled version of hello-react.jsx.
5 Calling React4xp.CLIENT.hydrate, the hydration of the entry along with a path pointer to the entry (we’ll get back to this below, as the concept of jsxPath), the ID of the container element the entry is rendered into, and the props.

We only added the greetee prop in the controller. The other one, react4xpId, is the unique ID of the component, same as the container element ID. It’s always added as a prop for each entry, conveniently helping to separate multiple instances of the same component (e.g. allowing them to share a common redux store without meddling with each other’s state).

Client-side rendering

In this example we’ll create another part, similar to Hello React, but with these variations:

  • Use client side rendering with a clientRender flag, turning the entire output into client-side React component.

  • Renter into existing target element which comes from a Thymeleaf template before the entry is rendered into it.

  • Reuse the "hello-react" template created in the Hello React examples

  • Insert editorial data And use these through the props.

Files involved:
site/parts/
  hello-react/
    hello-react.jsx
  client-render/
    client-render.xml
    client-render.html
    client-render.es6

Code

The part definition is still pretty unremarkable. Only now there’s a greetee TextLine input field ready for some editorial text from Content Studio.

client-render.xml:
<part>
	<display-name>Client Rendering</display-name>
    <description>Client-side rendered react component</description>
	<form>
        <input name="greetee" type="TextLine">
            <label>Who or what should we greet?</label>
            <occurrences minimum="1" maximum="1"/>
            <default>world</default>
        </input>
    </form>
</part>

The thymeleaf template shows that a target container element can be anywhere in an HTML body - same as in vanilla react - as long as we point to it with a unique element ID. Here: "second-example-container".

client-render.html
<div class="clientrender-example">
    <h2>Client-side rendering example</h2>
    <p>Skips server-side rendering, and instead sets up client-side react to render the entry into the container below:</p>
    <div id="second-example-container"></div>
    <p>(And we're done)</p>
</div>
We’ve hardcoded the ID here and in the controller for clarity. The best practice however, would be to pass it to thymeleaf through the model - see Custom flow.

Moving on to the part controller, where the React4xp stuff happens:

client-render.es6:
const portal = require('/lib/xp/portal');
const React4xp = require('/lib/enonic/react4xp');
const thymeleaf = require('/lib/thymeleaf');

const view = resolve('client-render.html'); (1)

exports.get = function(request) {
    const component = portal.getComponent();

    const preExistingBody = thymeleaf.render(view, {}); (1)

    const props = {
        greetee: component.config.greetee (2)
    };

    const params = {
        (3)
        body: preExistingBody,
        id: 'second-example-container',
        clientRender: true,
    };

    const jsxPath = 'site/parts/hello-react/hello-react'; (4)

    return React4xp.render(jsxPath, props, request, params);

What’s happening here?

1 The HTML with the target container element is rendered,
2 We get the greetee value from XP and insert in into the props,
3 We’re adding some parameters as a fourth argument object to .render:
  • body is the HTML body we’re inserting the React entry into,

  • id is setting the ID, and targeting that container element in body,

    • A side note: in the first example, we didn’t pass an id or a body parameter to .render, but it still worked. They are both optional, React4XP generates what’s missing: If no body is found, React4xp will generate an empty HTML with a matching element ID. If there’s no id found either, a random number is used - or data from component if you used that in the entry argument. If there’s a body but it doesn’t have any elements with an ID matching the id parameter, an empty target container element is added at the end of body.

  • clientRender: if this is truthy, you get old-school client-side React rendering. .render doesn’t render the entry on the server-side, but leaves the target container unchanged and instead makes some page contributions that makes the browser build the entry into the target container.

  • There’s also an optional pageContributions field, for adding pre-existing page contributions to the ones that .render generates.

4 Here an important concept is introduced - the jsxPath:

JsxPath

A jsxPath is the name of an entry in React4xp. Remember how we used the component object to refer to the entry in the first example, which is the easiest way but only works if the entry is in the same folder and has the same name. Here we want to access the entry (the same entry we used before) from a different part’s folder - from anywhere actually. JsxPaths are name strings, not paths relative to the controller (so avoid ../ etc).

Rules of thumb for jsxPaths:

  • If an entry file is a JSX file under src/main/resources/site, the jsxPath is the file path relative to src/main/resources/ - unix-style and without file extension.

  • An entry can also be located under src/main/resources/react4xp/_entries, and then the jsxPath will be relative to that folder instead.

  • If you’re ever unsure: all the available jsxPaths are stored in build/resources/main/assets/react4xp/entries.json. This file is generated by webpack during build (and shouldn’t be deleted or edited).

Okay, enough - time to run the example!

Setup

  • The new part is ready. Add it to some content in Content Studio, the same way you did in Client-side rendering.

  • Change the part’s greetee field: edit the content, click and mark the "Client-side rendering example" part, and edit the "Who or what should we greet?" field on the right-side config panel. When you save it, the preview should update.

Changing props editorially in Content Studio

Output

If we open a Preview tab and dig into the output page source, it’s similar to the previous example:

<body>
  <main data-portal-region="main" class="xp-region">

    <div class="clientrender-example">
      <h2>Client-side rendering example</h2>
      <p>Skips server-side rendering, and instead sets up client-side react to render the entry into the container below:</p>
      <div id="serverside-example-container"></div> (1)
      <p>(And we're done).</p>
    </div>

  </main>

  <script src="(...) /react4xp/externals.88e80cab5.js" ></script>
  <script src="(...) /react4xp-client/" ></script>
  <script src="(...) /react4xp/site/parts/hello-react/hello-react.js"></script> (2)
  <script defer> (3)
    React4xp.CLIENT.render(
        React4xp['site/parts/hello-react/hello-react'],
        "second-example-container" ,
        {
            "greetee":"from the client side",
            "react4xpId":"second-example-container"
        }
    );
  </script>
</body>

Most notable differences from the first example:

1 The target container is now initially empty, there was no rendered HTML from the server.
2 The imported entry is still the same as in the previous example. This is the compiled version of the JSX component we pointed to with jsxPath in the controller.
3 Instead of .hydrate, we’re calling .render. Our react component is rendered and inserted into the "serverside-example-container" element by the browser. React4xp.CLIENT.render has the same signature as React4xp.CLIENT.hydrate: (entryJsxPath, targetElementId, props).

Custom flow

In this example we won’t use .render. Instead, we’ll look at a more direct syntax that gives you better control and opportunities for logic steps when you handle the entry.

We’ll construct a data-holding React4xp object from an entry, manipulate it a little, and use its built-in methods to render the body and the pageContributions separately.

Other features demonstrated:

  • The target container ID is inserted into body by Thymeleaf, which gets it from the data-holding object

  • Using an entry outside of XP’s /site/ structure, in the base directory /react4xp/_entries

  • Making some raw XP page contributions before rendering, with a script with inserted editorial data. Then passing that pageContributions object through .renderPageContributions, adding it to the React4XP page contributions

Files involved:
react4xp/_entries/
  ColorThing.jsx
site/parts/custom-flow/
  custom-flow.xml
  custom-flow.html
  custom-flow.es6

Code

The part definition now defines some other editorial data: color.

custom-flow.xml:
<part>
  <display-name>Custom Flow</display-name>
  <description>React4xp object syntax, and more</description>
  <form>
    <input name="color" type="TextLine">
      <label>What's the color of the thing?</label>
      <occurrences minimum="1" maximum="1"/>
      <default>red</default>
    </input>
  </form>
</part>

The react component is inline-styled with the color it gets from the props:

ColorThing.jsx:
import React from 'react';

export default (props) =>
  <div style={{border: `1px dotted ${props.color}`,margin:"5px",padding:"5px" }}>
    <h2>The {props.color} thing</h2>
    <p style={{color: props.color}}>Hey, I'm pretty {props.color}!</p>
  </div>;

The Thymeleaf view receives the ID of the target container element from Thymeleaf:

custom-flow.html
<div>
    <p>Here comes the thing:</p>
    <div data-th-id="${targetId}"></div>
</div>

Finally, the controller:

custom-flow.es6
const portal = require('/lib/xp/portal');
const React4xp = require('/lib/enonic/react4xp');
const thymeleaf = require('/lib/thymeleaf');

const view = resolve('custom-flow.html');

exports.get = function(request) {
    const component = portal.getComponent();

    const reactObj = new React4xp('ColorThing'); (1)

    reactObj
        .setProps({ color: component.config.color })
        .uniqueId(); (2)

    const model = {
        targetId: reactObj.react4xpId (3)
    };
    const preRenderedBody = thymeleaf.render(view, model);

    const preExistingPageContributions = {
        bodyEnd: `<script>
        	console.log('Okay, rendered the ${reactObj.props.color} thing.');
        		</script>`
    }; (3)

    return { (4)

        body: reactObj.renderBody({
            body: preRenderedBody, (5)
        }),

        pageContributions: (request.mode === 'live' || request.mode === 'preview') ? (6)
            reactObj.renderPageContributions({ (7)
                pageContributions: preExistingPageContributions
            }) :
            undefined
    }
};
1 Constructing the data-holding React4XP object reactObj. The constructor takes one mandatory argument: an entry reference. Just like the first argument in .render, the entry reference can be a jsxPath OR an XP component object. Here it’s a jsxPath. So why is ColorThing, and only that, the jsxPath to ColorThing.jsx? Because the JSX file is at the root level of the React4XP entries base folder: src/main/resources/react4xp/_entries.
2 The React4XP object has setter methods that return the object itself, so you can set any optional attributes with a chained builder pattern like in the example, or separately. The uniqueId() method forces the ID of the object and the target container element to be unique. If an object doesn’t have an ID, this will be called by default when rendering (so we could have just skipped it here). The order of the setter methods doesn’t matter. This example is equivalent to: reactObj.setProps({color: component.config.color}); reactObj.uniqueId();
3 We can read attributes from reactObj before the rendering. At this point, the ID (react4xpId) and props are set in it. We read them out and injecting them into the thymeleaf model and into some random page contributions.
4 Here, body and pageContributions are rendered separately (renderBody and renderPageContributions). Remember that this is different from .render which does everything in one go.
5 The Thymeleaf-rendered body is given to .renderBody, which will insert react into that HTML. Again, the body parameter is optional - if we don’t submit it .renderBody will just generate a target container for you, with a matching ID.
6 The other difference from .render: there’s no automatic selection of rendering mode here. If we want to avoid active client-side JS running in Content Studio’s edit and inline modes, we now need to do it manually: detect the viewing mode from the request object and just skip the pageContributions.
7 We render the necessary page contributions for activating the entry, and pass preExistingPageContributions through .renderPageContributions - just adding it to the rendered page contributions. The pageContributions parameter is optional.

Just like the .render method in examples 01 and 02, we can control .renderBody and .renderPageContributions with the clientRender parameter. In this example, we called them both without it (so body is rendered on the server-side into the HTML, and .hydrate is called on the body in the client, instead of .render). The clientRender parameter should match between the two rendering functions for a React4XP object: if we add it to .renderBody (and it’s true/truthy), we should add it to .renderPageContributions too.

Okay, let’s take a look!

Setup

Like before, open Enonic XP Content Studio, add the new part to some content, select and edit it and change the Color of the Thing, for example to "blue".

Save, it should look something like this:

Rendered text in Content Studio is blue, and says: the blue thing

Output

If we open it in Preview and look at the browser console, we also see that the little console.log script from the controller has picked up the color prop from the React4XP object:

Rendered preview looks the same as in Content Studio, but now we also see that the expected output was printed in the browser console.

The output page source should similar to this (and similar to what .render generated earlier):

<body>
    <main data-portal-region="main" class="xp-region">

        <div data-portal-component-type="part">
            <p>Here comes the thing:</p>
            <div></div>
            <div id="_99689402">
                <div style="border:1px dotted blue;margin:5px;padding:5px" data-reactroot="">
                    <h2>The <!-- -->blue<!-- --> thing</h2>
                    <p style="color:blue">Hey, I'm pretty <!-- -->blue<!-- --> !</p>
                </div>
            </div>
        </div>

    </main>
    <script src="(...) react4xp/externals.88e80cab5.js"></script>
    <script src="(...) react4xp-client/"></script>

    <script>console.log('Okay, rendered the blue thing.');</script>

    <script src="(...) react4xp/ColorThing.js"></script>
    <script defer>React4xp.CLIENT.hydrate(React4xp['ColorThing'], "_99689402", {
        "color": "blue",
        "react4xpId": "_99689402"
    });</script>
</body>
the random but matching ID (uniqueId) of the target container and in the React4xp.CLIENT.hydrate call.

Chaining

Demonstrating some final features:

  • We’ll stay with the syntax from the previous example, and that lets us make a part with multiple entries - both different entries and reusing multiple instances of the same entry.

    • Chaining: We’ll see how rendered bodies and page contributions are passed through all the entries, before returning the final body and pageContribution to the response object.

  • Importing other React components into your entries - both other entries and React components from dependency chunks:

    • Webpack compiles and packs code into "sub-libraries", for bundling up and optimizing code that’s frequently imported by other entries (or chunks).

Obviously, this is gonna be the most complex example. Also, some of the React components in this example will be stateful and active on the client (this doesn’t really demonstate anything - active components don’t require anything special from React4XP, we’ve just been using passive components until now).

Files involved:
react4xp/
  _entries/
    mySubfolder/ (1)
      BuilderClickerEntry.jsx
  myChunk/ (2)
    BuilderClicker.jsx
site/parts:
  /chaining/
    chaining.jsx
    chaining.xml
    chaining.html
    chaining.es6

Just make a mental note of a difference from before - two added subfolders:

1 mySubfolder under react4xp/_entries/
2 myChunk under react4xp/

They are important in two ways, we’ll see how in a moment.

Code

The first entry:

react4xp/_entries/mySubfolder/BuilderClickerEntry.jsx:
import React from 'react';

import BuilderClicker from '../../myChunk/BuilderClicker';

export default (props) => <div className="builderclicker-entry">
		<BuilderClicker {...props} />
	</div>;

Some repetition from before: remember how JSX files below react4xp/_entries will be compiled to entries, with a jsxPath relative to that folder and without the file extension? So this will be an entry with the jsxPath mySubfolder/BuilderClickerEntry.

It’s functionally pretty slim, it doesn’t do much except import another React component, react4xp/myChunk/BuilderClicker and pass the props down to it. Since BuilderClicker is not under react4xp/_entries, it’s not an entry and can’t be used by React4XP. Which is why it needs an entry like this.

A slightly heavier, non-entry React component:

react4xp/myChunk/BuilderClicker.jsx:
import React from 'react';

class BuilderClicker extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            first: props.first,
            second: props.second,
        }
    };

    // Doubles the 'first' or 'second' string in state, depending on the key.
    makeMore = (key) => {
        this.setState({[key]: this.state[key] + " " + this.state[key]});
    };

    render() {
        return <div className="builderclicker">
            <h3 style={{color:"green"}}>
                <span onClick={() => this.makeMore('first')}
                      style={{cursor: "pointer"}}
                      className="first">{this.state.first}
                </span> <span onClick={() => this.makeMore('second')}
                      style={{cursor: "pointer"}}
                      className="second">{this.state.second}</span>
            </h3>
        </div>;
    }
};

export default (props) => <BuilderClicker {...props} />;

BuilderClicker displays two texts in one line, and builds more by doubling each of the texts whenever they are clicked in the browser.

This is a non-entry React component, that will be imported by several of the entries in this example (BuilderClickerEntry is one of them). Because of its source file location, BuilderClicker will be compiled into a highly reuseable, optimized and auto-handled chunk called myChunk:

Chunks:

JSX files that are not under site/ or react4xp/_entries won’t be compiled into entries. They don’t have a jsxPath, and need to be imported by an entry to be used in React4XP (or to be precise: they must be part of an import tree with an entry on top).

Everything that’s imported from files in other subfolders below react4xp/ will be compiled into chunks. These are "sub-library" code bundles with the same name as the subfolder. They are optimized for repeated loading and runtime import, and cached for reuse, with a cache-busting content hash added to the file name.

Chunks are made to be fire-and-forget: you don’t need to handle them in any way after naming the subfolders and importing the contents correctly. React4xp takes care of them during serving, caching and server-side rendering.

One recommended usage - an entry as a bridge between React4xp and more heavyweight React components:

Since the chunks are most optimized, it’s recommended to keep the entries slim and put as much of the heavy and reusable stuff as possible into chunks. Also, it’s best to keep the non-entries in chunks: each JSX entry is compiled to its own separate JS file. If an entry imports a non-entry piece of code that’s not in a chunk either, it will just be compiled into the entry’s JS "bundle", making it more heavy-weight.

Another entry in the part:

site/parts/chaining/chaining.jsx:
import React from 'react';
import BuilderClickerEntry from '../../../react4xp/_entries/mySubfolder/BuilderClickerEntry';

export default (props) => <BuilderClickerEntry {...props} />;

Three things to note here:

  • An entry can import and nest another entry just fine (if you should ever need to),

  • The same React component, BuilderClicker, is imported from its chunk more than once in the same part (but only loaded once in the client),

  • The two instances of it are functional and independent in the client. The fact that it’s imported into the part through two different entries doesn’t matter for this - we’ll show this by using this entry twice in the controller.

The part definition doesn’t define anything editorial this time:

site/parts/chaining/chaining.xml:
<part>
  <display-name>Chaining</display-name>
  <description>Multiple react components, chaining, nesting, hydration</description>
  <form />
</part>

The part view has two target containers (with hardcoded IDs) and a horizontal divider, and clearly expects some more containers to be added:

site/parts/chaining/chaining.html:
<div class="chaining-example">
    <h1>04 - Chaining Example</h1>
    <p>These two target containers existed in the HTML:</p>
    <div id="a-target-container"></div>
    <div id="another-target-container"></div>

    <hr style="display:block; margin:20px; width:100%; height:1px; border:1px dotted #aaa;"/>
    <p>The rest of the containers don't exist before rendering, but are inserted at the end:</p>
</div>

And finally, the juicy part controller:

site/parts/chaining/chaining.es6:
const portal = require('/lib/xp/portal');
const thymeleaf = require('/lib/thymeleaf');
const React4xp = require('/lib/enonic/react4xp');

const view = resolve("chaining.html");

exports.get = function(request) {
    const component = portal.getComponent();

    const clientRender = (request.mode !== 'edit' && request.mode !== 'inline'); (1)


    const firstReact4xpObj = new React4xp('mySubfolder/BuilderClickerEntry') (2)
        .setId("a-target-container")
        .setProps({
            first: "Click",
            second: "ME!"
        });

    const secondReact4xpObj = new React4xp(component) (3)
        .setId("another-target-container")
        .setProps({
            first: "No click ME!",
            second: "I do the exact same thing only better!"
        });

    // ------------------------------ A horizontal separator comes here in the view:
    // a new section where React4XP generates and inserts target containers where the
    // IDs didn't exist in the HTML


    const thirdReact4xpObj = new React4xp(component) (4)
        .setId("a-third-container-doesnt-exist-but-will-be-generated") (5)
        .setProps({
            first: "Here I am.",
            second: "Again."
        });


    let body = thymeleaf.render(view, {});

    body = firstReact4xpObj.renderBody({ body }); (6)
    body = secondReact4xpObj.renderBody({ body, clientRender });
    body = thirdReact4xpObj.renderBody({ body });

    let pageContributions = firstReact4xpObj.renderPageContributions();
    pageContributions = secondReact4xpObj.renderPageContributions({
    	pageContributions,
    	clientRender
    });
    pageContributions = thirdReact4xpObj.renderPageContributions({ pageContributions });


    ['first', 'second', 'third', 'fourth'].forEach(cardinalNum => {	(7)
        const notUniqueComp = new React4xp(
            	'site/parts/hello-react/hello-react'
            )
            .setId('this-is-not-unique')
            .setProps({ greetee: `${cardinalNum} repeated thing`});

        body = notUniqueComp.renderBody({ body });
        pageContributions = notUniqueComp.renderPageContributions({ pageContributions });
    });


    ['first', 'second', 'third', 'fourth'].forEach(cardinalNum => {
        const uniqueComp = new React4xp(
            	'site/parts/hello-react/hello-react'
            )
            .setId('this-id-is-unique').uniqueId() (8)
            .setProps({ greetee: `${cardinalNum} unique thing`});

        body = uniqueComp.renderBody({body});
        pageContributions = uniqueComp.renderPageContributions({ pageContributions });
    });


    return { (9)
        body,
        pageContributions: clientRender ?
            pageContributions :
            undefined,
    };
};
1 Content Studion and client-side rendering/hydration: Just like we did in the previous example, it’s a good idea to respond to XP’s viewing mode: are the react components being displayed inside Content Studio (request.mode is 'edit' or 'inline')? If so, the client-side JS of Content Studio may clash with react’s JS. We’re making a boolean clientRender for common control of all the entries in this part. This allows client-side rendering and hydration only outside Content Studio, and makes React4xp render static and un-hydrated HTML visualizations inside Content Studio - giving a visualization everywhere but activation only outside of Content Studio. Repetition: the React4xp.render shorthand function does all this automatically, if you prefer that.
2 Importing BuilderClicker from myChunk a first time, through BuilderClickerEntry…​
3 …​and a sceond time, through the part’s own entry (referred by component) which nests BuilderClickerEntry…​
4 …​and a third time, through the same part’s own entry again.
5 From here on down, none of the IDs will exist as target element IDs in the body. React4XP auto-handles this by inserting them at the end of body, in the order of chaining:
6 Chaining: First creates a body starting point from the local Thymeleaf template. This is passed through the .renderBody method of all the React4XP objects, each one expanding body by inserting either just a container element (clientRender) or rendering more React into it. firstReact4xpObj and thirdReact4xpObj will be server-side-rendered, secondReact4xpObj will be client-side-rendered. Note how the clientRender parameter matches for each React4XP object, between the renderBody and .renderPageContributions calls in the next step. .renderPageContributions works the same way: a pageContributions object is expanded with activating scripts for each time it passes through a React4XP object. .renderPageContributions only appends what’s necessary, so that shared components and chunks etc aren’t loaded more than once in the client.
7 Making 4 unique React4XP objects from the same entry, with different props, and adding them to the chain. They all have the same ID ('this-is-not-unique'), so they will be rendered and overwritten into the same container element - so only the last one of them survives and is visible.
8 So the lesson is to force the IDs to be unique, by adding a .uniqueId() call in each iteration. Now that the IDs are different, and they each get their own container element and all four are visibly rendered.
9 And finally, outputting the results of the chain (although the pageContributions are skipped inside Content Studio).

In this example we’re making a new React4xp object for each rendering. An object can in principle be re-rendered, but rendering a body or pageContribution will lock the ID of the object.

In other words: we can use an entry multiple times target1ing different container elements by using different React4XP objects. And we can render the same React4XP object into the same container multiple times (if we should need to). But trying to change the ID and target element will throw an error after an object’s first rendering.

Setup

Add this part the same way as in the previous examples and look at it in the Preview - you should see this:

You clicked, Sir? I am but a humble screenshot!

The few things worth confirming here:

  • A many-to-many relationship: all of the entries and React4xp objects were visualized by a single part here. And some of the entries have been used in other parts and across several React4xp objects.

  • This is but a humble screenshot, but in the actual Preview outside of Content Studio, the green rendered BuilderClicker instances are now active and respond to clicks as defined in BuilderClicker: clicking the first or second half of each of them doubles the clicked text (this activation happened in the .hydrate steps for server-side rendered entries, and in the actual rendering for client-side rendered ones).

  • The clicks and reponses are isolated to the instance that was actually clicked: even though they are technically the same entry BuilderClicker, they each have their own state. This is not because BuilderClicker is nested differently in them, but because their ID’s are different,

  • There’s only one "…​repeated thing" but four "…​unique thing", as explained above (point 7 and 8 in the controller code)

Output

The output page source to the client is much longer now. If you want to dive into that, it would look something like this:

Huge page source:
<!DOCTYPE html>
<html>
    <head></head>

    <body>

        <main data-portal-region="main" class="xp-region">
            <div data-portal-component-type="part" class="chaining-example">
                <h1>04 - Chaining Example</h1>
                <p>These two target containers existed in the HTML:</p>

                <div id="a-target-container"> (1)
                    <div class="builderclicker-entry" data-reactroot="">
                        <div class="builderclicker">
                            <h3 style="color:green">
                                <span style="cursor:pointer" class="first">Click</span>
                                <span style="cursor:pointer" class="second">ME!</span>
                            </h3>
                        </div>
                    </div>
                </div>

                <div id="another-target-container"></div> (1)

                <hr style="display:block; margin:20px; width:100%; height:1px; border:0; border-bottom:1px dotted #aaa;"/>
                <p>The rest of the containers don't exist before rendering, but are generated and inserted at the end:</p>
                <br/>

                <div id="a-third-container-doesnt-exist-but-will-be-generated">  (2)
                    <div class="builderclicker-entry" data-reactroot="">
                        <div class="builderclicker">
                            <h3 style="color:green">
                                <span style="cursor:pointer" class="first">Here I am.</span>
                                <span style="cursor:pointer" class="second">Again.</span>
                            </h3>
                        </div>
                    </div>
                </div>

                <div id="this-is-not-unique">  (3)
                	<p data-reactroot="">Hello <!-- -->fourth repeated thing<!-- --> !</p>
                </div>

                <div id="this-id-is-unique_82264525">
                	<p data-reactroot="">Hello <!-- -->first unique thing<!-- --> !</p>
                </div>
                <div id="this-id-is-unique_92592361">
                	<p data-reactroot="">Hello <!-- -->second unique thing<!-- --> !</p>
                </div>
                <div id="this-id-is-unique_73808051">
                	<p data-reactroot="">Hello <!-- -->third unique thing<!-- --> !</p>
                </div>
                <div id="this-id-is-unique_54219185">
                	<p data-reactroot="">Hello <!-- -->fourth unique thing<!-- --> !</p>
                </div>
            </div>
        </main>

        <script src=" (...) /react4xp/externals.88e80cab5.js"></script>
        <script src=" (...) /react4xp-client/"></script>
        <script src=" (...) /react4xp/myChunk.b26b22ea4.js"></script>  (4)

        <script src=" (...) /react4xp/mySubfolder/BuilderClickerEntry.js"></script>
        <script defer>
            React4xp.CLIENT.hydrate(
                React4xp['mySubfolder/BuilderClickerEntry'],
                "a-target-container",
                {
                    "first": "Click",
                    "second": "ME!",
                    "react4xpId": "a-target-container"
                }
            );
        </script>

        <script src=" (...) /react4xp/site/parts/chaining/chaining.js"></script>  (5)
        <script defer>
            React4xp.CLIENT.render(
                React4xp['site/parts/chaining/chaining'],
                "another-target-container",
                {
                    "first": "No click ME!",
                    "second": "I do the exact same thing only better!",
                    "react4xpId": "another-target-container"
                }
            );
        </script>
        <script defer>
            React4xp.CLIENT.hydrate(
                React4xp['site/parts/chaining/chaining'],
                "a-third-container-doesnt-exist-but-will-be-generated",
                {
                    "first": "Here I am.",
                    "second": "Again.",
                    "react4xpId": "a-third-container-doesnt-exist-but-will-be-generated"
                }
            );
        </script>

        <script src=" (...) /react4xp/site/parts/hello-react/hello-react.js"></script> (5)
        <script defer>
            React4xp.CLIENT.hydrate(
                React4xp['site/parts/hello-react/hello-react'], (3)
                "this-is-not-unique",
                {
                    "greetee": "first repeated thing",
                    "react4xpId": "this-is-not-unique"
                }
            );
        </script>
        <script defer>
            React4xp.CLIENT.hydrate(
                React4xp['site/parts/hello-react/hello-react'],
                "this-is-not-unique",
                {
                    "greetee": "second repeated thing",
                    "react4xpId": "this-is-not-unique"
                }
            );
        </script>
        <script defer>
            React4xp.CLIENT.hydrate(
                React4xp['site/parts/hello-react/hello-react'],
                "this-is-not-unique",
                {
                    "greetee": "third repeated thing",
                    "react4xpId": "this-is-not-unique"
                }
            );
        </script>
        <script defer>
            React4xp.CLIENT.hydrate(
                React4xp['site/parts/hello-react/hello-react'],
                "this-is-not-unique",
                {
                    "greetee": "fourth repeated thing",
                    "react4xpId": "this-is-not-unique"
                }
            );
        </script>
        <script defer>
            React4xp.CLIENT.hydrate(
                React4xp['site/parts/hello-react/hello-react'],
                "this-id-is-unique_82264525",
                {
                    "greetee": "first unique thing",
                    "react4xpId": "this-id-is-unique_82264525"
                }
            );
        </script>
        <script defer>
            React4xp.CLIENT.hydrate(
                React4xp['site/parts/hello-react/hello-react'],
                "this-id-is-unique_92592361",
                {
                    "greetee": "second unique thing",
                    "react4xpId": "this-id-is-unique_92592361"
                }
            );
        </script>
        <script defer>
            React4xp.CLIENT.hydrate(
                React4xp['site/parts/hello-react/hello-react'],
                "this-id-is-unique_73808051",
                {
                    "greetee": "third unique thing",
                    "react4xpId": "this-id-is-unique_73808051"
                }
            );
        </script>
        <script defer>
            React4xp.CLIENT.hydrate(
                React4xp['site/parts/hello-react/hello-react'],
                "this-id-is-unique_54219185",
                {
                    "greetee": "fourth unique thing",
                    "react4xpId": "this-id-is-unique_54219185"
                }
            );
        </script>
    </body>
</html>

Just confirming what you surely guessed would happen:

1 While the first and third React4XP objects were server-side rendered and hydrated, the second one was client-side rendered into an empty container.
2 Several of the containers that were output weren’t defined in the original Thymeleaf template, and were only rendered because the ID wasn’t found.
3 Only one container with "this-is-not-unique" was rendered for the same reason: that ID had already been inserted. So all the corresponding React4XP objects were server-side rendered into that one. This would also happen with client-side rendering! Also look further down: the client is asked to hydrate all four instances, which will log errors in the console since the content doesn’t match.
4 React4xp automatically traced the dependency to myChunk.<hash>.js and added this import to the page contributions because that’s where BuilderClicker comes from.
5 Although several of the generated assets are used more than once, the page contributions are trimmed for duplicates so each of them are only downloaded to the client once.

Need to go deeper?

Thats most of what React4XP offers. For more technical details, some corner-case features, adaptability and more complex functionality, we will shortly release on the Enonic pages a full API overview and other in-depth documentation for the library and build setup.

Contents