An introduction to using React with H5P

React is described as a javascript library for building user interfaces. In this post, we will examine how developers can create React components in H5P. React synergizes well with H5P since both makes no assumptions about where it inserts elements. React can create standalone components, render them and attach them to the DOM.

The goal of this post is to rewrite a simple H5P library called Greeting Card with React Components to get you started producing content on your own. The post is structured as a sequence of steps.

  1. A simple React component
  2. Getting started with Greeting Card and React
  3. Rewriting Greeting Card image element
  4. Creating a composite React component
  5. Rendering Greeting Card

The benefits of using React are opinionated and I will only touch lightly upon them since there already exists an abundance of resources on this.

Why use React with H5P ?

The key point to be made for React is its way of creating components that unifies markup and its corresponding view logic. React argues that crafting fully encompassed components makes more sense than the more traditional way of creating markup, then binding logic to the markup, often resulting in connected parts being spread across different files. With the ability to build abstractations and unifying details on a component React claims to make views easier to extend and maintain.

Another concern is performance. When changes are made to a component, React determines the minimal set of changes to be applied to the DOM, and only changes these which results in fast re-renders. In highly dynamic content, such as in H5Ps, performance is essential. However, in the end it boils down to developer preferences, for further inspiration you can read why React was built. Without further ado, let's dive into our first React component.

A simple React component

Let's have a look at the simplest example taken from React's introduction written in JSX. The example reveals that a React component is created using a special createClass function that requires a render function. The render function must return the element that will be rendered. Later the component can be rendered using ReactDOM's render function.

var HelloMessage = React.createClass({
  render: function() {
    return <div>Hello {this.props.name}</div>;
  }
});

ReactDOM.render(<HelloMessage name="John" />, mountNode);

Writing in JSX is entirely optional, and the snippet from our example can be compiled to the following JS:

"use strict";

var HelloMessage = React.createClass({
  displayName: "HelloMessage",

  render: function render() {
    return React.createElement(
      "div",
      null,
      "Hello ",
      this.props.name
    );
  }
});

ReactDOM.render(React.createElement(HelloMessage, { name: "John" }), mountNode);

Examples will be written in JSX, you can use Babeljs.io's REPL if you wish to compile it to JS.

 

Getting started with Greeting Card and React

You can grab Greeting Card by following our tutorial for creating libraries or at GitHub. Once you have it, you can upload it to your local server following the development environment guide.

To be able to access the React and ReactDOM from within Greeting Card we need to create a dependency to the React H5P wrapper. This is done by uploading the React wrapper library from GitHub and creating a dependency to it in Greeting Card's library.json as follows:

{
  "title": "Greeting card",
  "description": "Displays a greeting card",
  "majorVersion": 1,
  "minorVersion": 0,
  "patchVersion": 3,
  "runnable": 1,
  "author": "Joubel AS",
  "license": "MIT",
  "machineName": "H5P.GreetingCard",
  "preloadedJs": [
    {
      "path": "greetingcard.js"
    }
  ],
  "preloadedCss": [
    {
      "path": "greetingcard.css"
    }
  ],
  "preloadedDependencies": [
    {
      "machineName": "ReactJS",
      "majorVersion": 1,
      "minorVersion": 0
    }
  ]
}

 

Rewriting Greeting Card image element

We will focus our attention on rewriting the attach function of Greeting Card since this is where all elements are created and attached. We want React to handle this. The attach function contains two main elements, an image and a greeting text:

  /**
   * Attach function called by H5P framework to insert H5P content into
   * page
   *
   * @param {jQuery} $container
   */
  C.prototype.attach = function ($container) {
    var self = this;
    // Set class on container to identify it as a greeting card
    // container.  Allows for styling later.
    $container.addClass("h5p-greetingcard");
    // Add image if provided.
    if (this.options.image && this.options.image.path) {
      $container.append('<img class="greeting-image" src="' + H5P.getPath(this.options.image.path, this.id) + '">');
    }
    // Add greeting text.
    $container.append('<div class="greeting-text">' + this.options.greeting + '</div>');
    
    // TODO - need to wait for image beeing loaded
    // For now using timer. Should wait for image is loaded...
    setTimeout(function () {
      self.$.trigger('resize');
    }, 1000);
  };

The image element is created and appended to the wrapper if an image is created for this Greeting Card instance. The process can be isolated to the following lines:

    // Add image if provided.
    if (this.options.image && this.options.image.path) {
      $container.append('<img class="greeting-image" src="' + H5P.getPath(this.options.image.path, this.id) + '">');
    }

This is a compact format that creates and appends the image in the same line. The main philosophy for DOM manipulation is to do as few operations as possible because they are quite expensive. Instead of appending right after we have created the image we want to create the whole DOM structure, then insert it all in one operation. We will break the image code into two smaller chunks, namely creating the image, then rendering it. For simplicity, we will first disregard the conditional statement and only create the image element. This is done by returning the HTML for our image in the render function:

    var Image = React.createClass({
      render: function () {
        return <img className="greeting-image" src={H5P.getPath(this.props.options.image.path, this.props.id)} />
      }
    });

Notice that parameters passed to the component is accessed through the props object within the component. The parameter is passed to the component the same way we saw in the React introduction example. Also, notice that JavaScript expressions need to be encapsulated in curly brackets to get evaluated. Proceeding, we will include the conditional statement inside the render function. A render function that returns false will not be rendered. Knowing this we can modify our component to return the image node if we have the required image path, otherwise return false:

    var Image = React.createClass({
      render: function () {
        if (this.props.options.image.path && this.props.id) {
          return <img className="greeting-image" src={H5P.getPath(this.props.options.image.path, this.props.id)} />
        }
        return false;
      }
    });

To render the Image component we use ReactDOM and pass the parameters using curly braces as seen earlier.

    ReactDOM.render(<Image options={self.options} id={self.id} />, $container.get(0));

Taking our newly formed React image component and replacing it with our old implementation your attach function should now look like this:

/**
   * Attach function called by H5P framework to insert H5P content into
   * page
   *
   * @param {jQuery} $container
   */
  C.prototype.attach = function ($container) {
    var self = this;
    // Set class on container to identify it as a greeting card
    // container.  Allows for styling later.
    $container.addClass("h5p-greetingcard");
    // Add image if provided.
     var Image = React.createClass({
      render: function () {
        if (this.props.options.image.path && this.props.id) {
          return <img className="greeting-image" src={H5P.getPath(this.props.options.image.path, this.props.id)} />
        }
        return false;
      }
    });
    
    ReactDOM.render(<Image options={self.options} id={self.id} />, $container.get(0));

    
    // Add greeting text.
    $container.append('<div class="greeting-text">' + this.options.greeting + '</div>');
    
    // TODO - need to wait for image beeing loaded
    // For now using timer. Should wait for image is loaded...
    setTimeout(function () {
      self.$.trigger('resize');
    }, 1000);
  };

 

Creating a composite React component

When we are done celebrating our brand new React Component, we can proceed with the remainder of the attach function. There are two elements left, the container and the greeting card text element:

    // Set class on container to identify it as a greeting card
    // container.  Allows for styling later.
    $container.addClass("h5p-greetingcard");
    // Add greeting text.
    $container.append('<div class="greeting-text">' + this.options.greeting + '</div>');

We will address both elements and the issue of inserting DOM once with a composite React component. We will call this new component GreetingCard. We should try to make sure that all elements are encompassed by this component, so we only have to render once. Using our newfound wisdom we will construct a simple container component:

var GreetingCard = React.createClass({
  render: function () {
	return <div className="h5p-greetingcard"></div>
  }
});

Adding Image and text inside GreetingCard is done by placing them inside the division:

var GreetingCard = React.createClass({
  render: function () {
	return <div className="h5p-greetingcard">
	  <Image />
	  <div className="greeting-text"></div>
	</div>
  }
});

We must not forget that Image and greeting-text require some variables to be rendered. These are sent as parameters the same way we did when rendering Image. This means composite components requires all parameters of their children:

var GreetingCard = React.createClass({
  render: function () {
	return <div className="h5p-greetingcard">
	  <Image options={this.props.options} id={this.props.id}/>
	  <div className="greeting-text">{this.props.options.greeting}</div>
	</div>
  }
});

 

Rendering Greeting Card

Rendering our ReactDOM with the correct parameters becomes:

ReactDOM.render(<GreetingCard options={self.options} id={self.id} />, $container.get(0));

Pulling it all together, our new attach function should look like this:

  /**
   * Attach function called by H5P framework to insert H5P content into
   * page
   *
   * @param {jQuery} $container
   */
  C.prototype.attach = function ($container) {
    var self = this;
    // Set class on container to identify it as a greeting card
    // container.  Allows for styling later.
	
	var Image = React.createClass({
	  render: function () {
  		if (this.props.options.image.path && this.props.id) {
  		  return <img className="greeting-image" src={H5P.getPath(this.props.options.image.path, this.props.id)} />
  		}
  		return false;
	  }
	});

	var GreetingCard = React.createClass({
	  render: function () {
	    return (
  		  <div className="h5p-greetingcard">
		    <Image options={this.props.options} id={this.props.id}/>
		    <div className="greeting-text">{this.props.options.greeting}</div>
		  </div>
	    )
	  }
	});

    ReactDOM.render(<GreetingCard options={self.options} id={self.id} />, $container.get(0));
	
    // TODO - need to wait for image beeing loaded
    // For now using timer. Should wait for image is loaded...
    setTimeout(function () {
      self.$.trigger('resize');
    }, 1000);
  };

For completeness, here is the JS compiled equivalent:

/**
 * Attach function called by H5P framework to insert H5P content into
 * page
 *
 * @param {jQuery} $container
 */
"use strict";

C.prototype.attach = function ($container) {
  var self = this;
  // Set class on container to identify it as a greeting card
  // container.  Allows for styling later.

  var Image = React.createClass({
    displayName: "Image",

    render: function render() {
      if (this.props.options.image.path && this.props.id) {
        return React.createElement("img", { className: "greeting-image", src: H5P.getPath(this.props.options.image.path, this.props.id) });
      }
      return false;
    }
  });

  var GreetingCard = React.createClass({
    displayName: "GreetingCard",

    render: function render() {
      return React.createElement(
        "div",
        { className: "h5p-greetingcard" },
        React.createElement(Image, { options: this.props.options, id: this.props.id }),
        React.createElement(
          "div",
          { className: "greeting-text" },
          this.props.options.greeting
        )
      );
    }
  });

  ReactDOM.render(React.createElement(GreetingCard, { options: self.options, id: self.id }), $container.get(0));

  // TODO - need to wait for image beeing loaded
  // For now using timer. Should wait for image is loaded...
  setTimeout(function () {
    self.$.trigger('resize');
  }, 1000);
};

The final version of GreetingCard with React can be found at GitHub along with the React wrapper. I hope you liked this introduction to using React with H5P and hope it was easy to follow along.

Happy coding! :)

 

Did you like the post? Feel free to leave any feedback, questions or comments below.