Getting Started with React - Enterprise Java Content management system - Hippo CMS

Getting Started with React

This feature is available since Hippo CMS 12.3.0

Overview

In this tutorial, we will create a React app consuming Page Model JSON API based on a standard Hippo CMS project. The React app will read component metadata, models and content items to render Banner components, which are dynamically added and removed in Channel Manager.

Create a Hippo CMS Project

First, create a Hippo CMS project by following Getting Started With Hippo CMS. Add features to the project through Essentials: Banners, Events and News at least. See Add Features to the Project for detail.

Add Example Banner to Homepage

We need to add a Banner component in the Channel Manager for our demo. Open the Channel Manager to edit the default channel.

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto01.png

Select the Home Page Main container and add Banner component there.

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto02.png

Open the Parameter Setting dialog on the Banner component you've just added, and select a Banner document. e.g, banners/banner1.

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto03.png

Publish the channel.

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto04.png

If you visit http://localhost:8080/site/, then the homepage will show the Banner component content:

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto05.png

We will convert the channel to an SPA Site Channel in the following step.

Configure SPA Site and Page Model API

In the hst:root mount for the default channel, configure the following properties:

/hst:hst/hst:hosts/dev-localhost/localhost/hst:root:
  # SNIP
  hst:namedpipeline: SpaSitePipeline
  hst:pagemodelapi: resourceapi
  hst:responseheaders: ['Access-Control-Allow-Origin: http://localhost:3000', 'Access-Control-Allow-Credentials: true']

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto06.png

We've converted the root mount to an SPA Site Mount and set the mount path of Page Model JSON API to resourceapi. See Configure SPA++ for detail.

Also, we set CORS response headers (['Access-Control-Allow-Origin: http://localhost:3000', 'Access-Control-Allow-Credentials: true']) in order to allow a React node.js server (at http://localhost:3000) to consume Page Model JSON API (at http://localhost:8080/site/resourceapi/). This is very helpful in local development environment.

After writing changes to repository, you will be able to see Page Model JSON API working:

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto07.png

We've just completed setting up a SPA Site Channel and its Page Model JSON API mount.

Create React App

Now, let's create a React app. In a command line console, move to your Hippo CMS project's root folder, and create a React app using the following command (see Create React App for detail):

$ npx create-react-app myapp

At the moment this document is being written, it creates a React app with the following dependencies as shown in package.json:

{
  "name": "myapp",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.3.2",
    "react-dom": "^16.3.2",
    "react-scripts": "1.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}

The dependencies will probably change as time goes by, but the main principles in this document will apply almost the same.

Our React app was created in a myapp subfolder under the Hippo CMS project root folder. You can start the React app using yarn start in the myapp subfolder:

$ cd myapp
$ yarn start

This will open the React app at http://localhost:3000/ in your default browser automatically. The React app will show a welcome message for now. We will improve the React app to consume Page Model JSON API in the following sections.

Update App Class to Render State

Let's refactor the default App class in myapp/src/App.js a little bit to render the title and introduction from the state. We will update the state dynamically in the next section, but let's hard-code the state data for now and let the JSX elements read the state data like the following:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {

  constructor() {
    super();
    this.state = {
      name: "home",
      title: "Home Page",
      introduction: "Every website comes with its own styling requirements.",
      aggregatedPageModel: {},
      containerItemComponents: []
    };
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">{this.state.title} ({this.state.name})</h1>
        </header>
        <p className="App-intro">
          {this.state.introduction}
        </p>
      </div>
    );
  }
}

export default App;

This change will update the SPA page at http://localhost:3000/ automatically, showing the new title, "Home page (home)", and new introduction.

Fetch Dyanamic Data to Update State

The page still shows hard-coded title, name and introduction at the moment. Let's try to consume the dynamic content from Page Model JSON API. Add componentDidMount() function like the following:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {

  constructor() {
    super();
    this.state = {
      name: "home",
      title: "Home Page",
      introduction: "Every website comes with its own styling requirements.",
      aggregatedPageModel: {},
      containerItemComponents: []
    };
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">{this.state.title} ({this.state.name})</h1>
        </header>
        <p className="App-intro">
          {this.state.introduction}
        </p>
      </div>
    );
  }

  componentDidMount() {
    fetch('http://localhost:8080/site/resourceapi/')
    .then(results => results.json())
    .then(data => {
      this.setState({
        name: data.page.name,
        aggregatedPageModel: data,
        containerItemComponents: data.page.components[0].components[0].components
      });
    })
  }
}

export default App;

When componentDidMount lifecycle method is executed, the component was already rendered with the render() method, but the component would render itself again when the states are updated by the feched data through setState(...). In the above example, it sets name, aggregatedPageModel and containerItemComponents properties through #setState(...) from the JSON response. As a result, the render() method will be re-invoked after fetching the dynamic data from http://localhost:8080/site/resourceapi/.

As we didn't render any components such as the Banner component, this improvement wouldn't make a big difference yet, except of changing the title from "Home page (home)" to "Home page (homepage)". "home" is a hard-coded string whereas "homepage" is read from the the Page Model JSON API dynamically.

Add Banner Component

Now let's add the Banner React component which reads and renders the Banner component and associated content. Update  myapp/src/App.js like the following:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class BannerComponent extends Component {

  render() {
    const bannerDocRef = this.props.component.models.document.$ref.replace(/^\/content\//, '');
    const bannerDoc = this.props.aggregatedPageModel.content[bannerDocRef];
    const imageRef = bannerDoc.image.$ref.replace(/^\/content\//, '');
    const imageDoc = this.props.aggregatedPageModel.content[imageRef];
    const imageSrc = 'http://localhost:8080' + imageDoc._links.site.href;

    return (
      <div>
        <h2>{bannerDoc.title}</h2>
        <div>
          <img src={imageSrc} />
        </div>
      </div>
    );
  }
}

class App extends Component {

  constructor() {
    super();
    this.state = {
      name: "home",
      title: "Home Page",
      introduction: "Every website comes with its own styling requirements.",
      aggregatedPageModel: {},
      containerItemComponents: []
    };
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">{this.state.title} ({this.state.name})</h1>
        </header>
        <p className="App-intro">
          {this.state.introduction}
        </p>
        <div>
          {
            this.state.containerItemComponents.map((containerItemComponent) => {
              return <BannerComponent aggregatedPageModel={this.state.aggregatedPageModel} component={containerItemComponent} />;
            })
          }
        </div>
      </div>
    );
  }

  componentDidMount() {
    fetch('http://localhost:8080/site/resourceapi/')
    .then(results => results.json())
    .then(data => {
      this.setState({
        name: data.page.name,
        aggregatedPageModel: data,
        containerItemComponents: data.page.components[0].components[0].components
      });
    })
  }
}

export default App;

In this updated example, we did the following:

  • In App#componentDidMount() method, it sets aggregatedPageModel state property to the whole JSON model, and sets containerItemComponents state property to the first container component's child components only, for simplicity in this demo application.
  • In App#render() method, it creates BannerComponent objects to render.
  • BannerComponent class' render() method reads its component metadata and models and referenced content items, and finally renders the banner title and banner image element.

As the BannerComponent objects are added dynamically depending on the containerItemComponents state property, if you add one more Banner component in Channel Manager, the React app will update the page with two Banner components automatically as well:

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto08.png

More Advanced Examples

We have learned how to develop a React SPA which consumes the Page Model JSON API and renders content dynamically with Banner component examples.

This example could look too simple, but you may apply similar approaches in your project by adding more specified React components for News or Events components. Furthermore, you may consider adding React components for a generic HST Container Component or Container Item Component to reflect the real structure of HST page and components.

Also, if you need a seamless integration with the Channel Manager SPA API, you will need more considerations in each React component rendering.

For more details on those advanced topics, see Demo Projects which contains all the examples to cover the cases mentioned above.

Getting Started with React

This feature is available since Hippo CMS 12.3.0

Overview

In this tutorial, we will create a React app consuming Page Model JSON API based on a standard Hippo CMS project. The React app will read component metadata, models and content items to render Banner components, which are dynamically added and removed in Channel Manager.

Create a Hippo CMS Project

First, create a Hippo CMS project by following Getting Started With Hippo CMS. Add features to the project through Essentials: Banners, Events and News at least. See Add Features to the Project for detail.

Add Example Banner to Homepage

We need to add a Banner component in the Channel Manager for our demo. Open the Channel Manager to edit the default channel.

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto01.png

Select the Home Page Main container and add Banner component there.

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto02.png

Open the Parameter Setting dialog on the Banner component you've just added, and select a Banner document. e.g, banners/banner1.

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto03.png

Publish the channel.

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto04.png

If you visit http://localhost:8080/site/, then the homepage will show the Banner component content:

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto05.png

We will convert the channel to an SPA Site Channel in the following step.

Configure SPA Site and Page Model API

In the hst:root mount for the default channel, configure the following properties:

/hst:hst/hst:hosts/dev-localhost/localhost/hst:root:
  # SNIP
  hst:namedpipeline: SpaSitePipeline
  hst:pagemodelapi: resourceapi
  hst:responseheaders: ['Access-Control-Allow-Origin: http://localhost:3000', 'Access-Control-Allow-Credentials: true']

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto06.png

We've converted the root mount to an SPA Site Mount and set the mount path of Page Model JSON API to resourceapi. See Configure SPA++ for detail.

Also, we set CORS response headers (['Access-Control-Allow-Origin: http://localhost:3000', 'Access-Control-Allow-Credentials: true']) in order to allow a React node.js server (at http://localhost:3000) to consume Page Model JSON API (at http://localhost:8080/site/resourceapi/). This is very helpful in local development environment.

After writing changes to repository, you will be able to see Page Model JSON API working:

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto07.png

We've just completed setting up a SPA Site Channel and its Page Model JSON API mount.

Create React App

Now, let's create a React app. In a command line console, move to your Hippo CMS project's root folder, and create a React app using the following command (see Create React App for detail):

$ npx create-react-app myapp

At the moment this document is being written, it creates a React app with the following dependencies as shown in package.json:

{
  "name": "myapp",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.3.2",
    "react-dom": "^16.3.2",
    "react-scripts": "1.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}

The dependencies will probably change as time goes by, but the main principles in this document will apply almost the same.

Our React app was created in a myapp subfolder under the Hippo CMS project root folder. You can start the React app using yarn start in the myapp subfolder:

$ cd myapp
$ yarn start

This will open the React app at http://localhost:3000/ in your default browser automatically. The React app will show a welcome message for now. We will improve the React app to consume Page Model JSON API in the following sections.

Update App Class to Render State

Let's refactor the default App class in myapp/src/App.js a little bit to render the title and introduction from the state. We will update the state dynamically in the next section, but let's hard-code the state data for now and let the JSX elements read the state data like the following:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {

  constructor() {
    super();
    this.state = {
      name: "home",
      title: "Home Page",
      introduction: "Every website comes with its own styling requirements.",
      aggregatedPageModel: {},
      containerItemComponents: []
    };
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">{this.state.title} ({this.state.name})</h1>
        </header>
        <p className="App-intro">
          {this.state.introduction}
        </p>
      </div>
    );
  }
}

export default App;

This change will update the SPA page at http://localhost:3000/ automatically, showing the new title, "Home page (home)", and new introduction.

Fetch Dyanamic Data to Update State

The page still shows hard-coded title, name and introduction at the moment. Let's try to consume the dynamic content from Page Model JSON API. Add componentDidMount() function like the following:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {

  constructor() {
    super();
    this.state = {
      name: "home",
      title: "Home Page",
      introduction: "Every website comes with its own styling requirements.",
      aggregatedPageModel: {},
      containerItemComponents: []
    };
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">{this.state.title} ({this.state.name})</h1>
        </header>
        <p className="App-intro">
          {this.state.introduction}
        </p>
      </div>
    );
  }

  componentDidMount() {
    fetch('http://localhost:8080/site/resourceapi/')
    .then(results => results.json())
    .then(data => {
      this.setState({
        name: data.page.name,
        aggregatedPageModel: data,
        containerItemComponents: data.page.components[0].components[0].components
      });
    })
  }
}

export default App;

When componentDidMount lifecycle method is executed, the component was already rendered with the render() method, but the component would render itself again when the states are updated by the feched data through setState(...). In the above example, it sets name, aggregatedPageModel and containerItemComponents properties through #setState(...) from the JSON response. As a result, the render() method will be re-invoked after fetching the dynamic data from http://localhost:8080/site/resourceapi/.

As we didn't render any components such as the Banner component, this improvement wouldn't make a big difference yet, except of changing the title from "Home page (home)" to "Home page (homepage)". "home" is a hard-coded string whereas "homepage" is read from the the Page Model JSON API dynamically.

Add Banner Component

Now let's add the Banner React component which reads and renders the Banner component and associated content. Update  myapp/src/App.js like the following:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class BannerComponent extends Component {

  render() {
    const bannerDocRef = this.props.component.models.document.$ref.replace(/^\/content\//, '');
    const bannerDoc = this.props.aggregatedPageModel.content[bannerDocRef];
    const imageRef = bannerDoc.image.$ref.replace(/^\/content\//, '');
    const imageDoc = this.props.aggregatedPageModel.content[imageRef];
    const imageSrc = 'http://localhost:8080' + imageDoc._links.site.href;

    return (
      <div>
        <h2>{bannerDoc.title}</h2>
        <div>
          <img src={imageSrc} />
        </div>
      </div>
    );
  }
}

class App extends Component {

  constructor() {
    super();
    this.state = {
      name: "home",
      title: "Home Page",
      introduction: "Every website comes with its own styling requirements.",
      aggregatedPageModel: {},
      containerItemComponents: []
    };
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">{this.state.title} ({this.state.name})</h1>
        </header>
        <p className="App-intro">
          {this.state.introduction}
        </p>
        <div>
          {
            this.state.containerItemComponents.map((containerItemComponent) => {
              return <BannerComponent aggregatedPageModel={this.state.aggregatedPageModel} component={containerItemComponent} />;
            })
          }
        </div>
      </div>
    );
  }

  componentDidMount() {
    fetch('http://localhost:8080/site/resourceapi/')
    .then(results => results.json())
    .then(data => {
      this.setState({
        name: data.page.name,
        aggregatedPageModel: data,
        containerItemComponents: data.page.components[0].components[0].components
      });
    })
  }
}

export default App;

In this updated example, we did the following:

  • In App#componentDidMount() method, it sets aggregatedPageModel state property to the whole JSON model, and sets containerItemComponents state property to the first container component's child components only, for simplicity in this demo application.
  • In App#render() method, it creates BannerComponent objects to render.
  • BannerComponent class' render() method reads its component metadata and models and referenced content items, and finally renders the banner title and banner image element.

As the BannerComponent objects are added dynamically depending on the containerItemComponents state property, if you add one more Banner component in Channel Manager, the React app will update the page with two Banner components automatically as well:

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/concepts/spa-plus/howto/react/spareacthowto08.png

More Advanced Examples

We have learned how to develop a React SPA which consumes the Page Model JSON API and renders content dynamically with Banner component examples.

This example could look too simple, but you may apply similar approaches in your project by adding more specified React components for News or Events components. Furthermore, you may consider adding React components for a generic HST Container Component or Container Item Component to reflect the real structure of HST page and components.

Also, if you need a seamless integration with the Channel Manager SPA API, you will need more considerations in each React component rendering.

For more details on those advanced topics, see Demo Projects which contains all the examples to cover the cases mentioned above.