How-To: Using React.js with HOOPS Communicator

As a developer using HC, I would like to have guidance on how to get HC WebViewer up and running within a React application.

Introduction

React is a popular frontend library for UI and many prospects and partners want to use HOOPS technology within their existing or new React stack. This article aims to explain how the HOOPS WebViewer fits into the a React based application.

Note: This process pertains to the class based component design in React, and does not cover how to integrate HOOPS using Hooks in React. Using React Hooks with HC will be covered in a future article.

Instructions

If you prefer to learn by video, you can view the virtual training on this topic below:


This example will assume the environment created from the Create React App (CRA) environment (downloaded via npm - Create a New React App – React ). This package is the recommended and best way to get started with a new single-page React application. If you are integrating HOOPS into an existing package, you should be able to pick out the vital bits of information while going along from Create React App.

Once you have your React environment set up (either from CRA or from your existing project), you will want to locate your main index.html file that will be served when you first request your applications site. For a CRA app, this will reside in the public folder of the application root directory. This file should include the main <html> element along with the <head> and <body> elements. Within the <head> element, you’ll want to reference the hoops_web_viewer.js script, in the same manner shown within the documentation here.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />

    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

    <script src="%PUBLIC_URL%/hoops/hoops_web_viewer_monolithic.js"></script>

    <title>HC Using React.js</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

Here, we have chosen to use the monolithic version of the WebViewer library; however, using the WebAssembly compatible version will work with React as well. Just remember to include the additional files in the <head> as described here.

Note: With the default CRA setup, the hoops WebViewer library cannot be imported using the ES6 import syntax due to how Babel is configured. You can either access the Communicator object globally from the window Javascript object, or add the following to your webpack.config.js file:

  • Since hoops_web_viewer.js is an external library, we need to modify the webpack dependency configurations. Inside webpack.config.js, add the following to the return() function of module.exports:
externals: {
  communicator: ‘Communicator'
}

t’s now take a look at the Javascript code.

HOOPS Communicator requires a <div> element with a unique id tag within the HTML. Therefore, within one of our React class render methods, we have to tell React to render this <div> element.

You can choose to encapsulate the HOOPS functionality as you desire, but one way you could organize your code is to create a HOOPS Component. Using the basic scaffolding of the default CRA application, lets take a look at the App.js[x] file. I have modified the file to reference the React and HOOPS Communicator documentation in the header element.

I would now like to add a WebViewer canvas where my model will render. I am going to create a React Component called Hoops. and have it Render from the App Component. Create an empty JSX file called Hoops.jsx and be sure to import this file into the App component script.

import React from 'react';
import logo from './logo.svg';
import './App.css';

import Hoops from './Hoops';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          React.js Documentation
        </a>
        <a
          className="App-link"
          href="https://docs.techsoft3d.com/communicator/latest/build/overview-technical-overview.html"
          target="_blank"
          rel="noopener noreferrer"
        >
          HOOPS Communicator Documentation
        </a>
      </header>
      <Hoops/>
    </div>
  );
}

export default App;

Now within the Hoops.jsx file, we can write code to actually instantiate our WebViewer object. Organizational choices above aside, this is the basic Javascript code that your application will need to get a viewer going inside React.

To avoid having to muddy up the main index.html file, we want to leave the rendering of most DOM elements to React. We need a <div> element with an id for our viewer to render into, so we could either make a div element in a parent component, or we can leverage React state to keep the logic internal to our Component. In a viewer centric application, other components may rely on the state of the viewer. We want to be sure our WebViewer object is instantiated and good to go before rendering these other Components. In our example, we will use a toolbar containing actions on the WebViewer. We do not want to expose these actions before the WebViewer is loaded. In our constructor for the Hoops component, I will set a flag indicating whether the viewer has been instantiated or not.

In our main render() method, we will add a parent container div, and inside we will make our WebViewer target <div> with an id “canvas”. This is the id we will pass when instantiating the WebViewer. We will also add some conditional code to determine if we should render the dependent Components or not, based off the WebViewer status.

Because the React manages the updating of the DOM, we need to first tell React to render our “canvas” <div>, wait for it to mount to the DOM, and then instantiate our viewer. To do this, we should use the componentDidMount() callback within React. The render method will call, mount the <div> to the DOM, then fire the componentDidMount callback logic. I have abstracted the viewer lifecycle management into its own class, which will instantiate the WebViewer giving the div id (you could add additional lifecycle methods to this class).

export default class ViewerManager {

    createViewer(divId) {
    
        let viewerPromise = new Promise((resolve) => {
            
            // For use with SCS workflow
            
            let _viewer = new window.Communicator.WebViewer({
                containerId: divId,
                endpointUri: "models/microengine.scs",
            });
            _viewer.start();
            resolve(_viewer);
        });

        return viewerPromise;
    }

}

Once the viewer is instantiated, we will set the React state to indicate so. This will cause the render method to update since the Component state changed. It will recognize the viewer is instantiated, and then render the children Components like the UI toolbar and information pane.

import React from "react";
import ViewerManager from "./ViewerManager";
import UIToolbar from "./UIToolbar";
import InformationPane from "./InformationPane"

class Hoops extends React.Component {

    constructor() {
        super();
        this.state = {hwvInstantiated: false};
        this._viewer = undefined;
        this._hwvManager = new ViewerManager();
    }

    // Once the element is in the browser DOM, initialize the viewer and set up sockets
    componentDidMount() {

        this._hwvManager.createViewer("canvas")
        .then( (viewer) => {
            this._viewer = viewer;
            // Once the viewer is instantiated, we can set the state to true to have the React update the DOM
            this.setState({
                hwvInstantiated: true
            });

            // Storing the callback in its own function to avoid registering a bound callback 
            // (more difficult to unregister that in HC)
            this._viewer.setCallbacks({
                modelStructureReady: () => this._viewer.fitWorld(),
                selectionArray: () => console.log("Selection Changed"),
            });
            window.addEventListener("resize", () => this._viewer.resizeCanvas());

        });
    }

    render () {
        let viewerElement;
        // We must render a div for the viewer to be placed in. This component must mount before instantiating the viewer.
        // Once the component has mounted and the viewer is instantiated, we can render other components that rely on it.
        if (!this.state.hwvInstantiated) {
            viewerElement = <div></div>;
        }

        else {
            // Now that the viewer is instantiated, we can render any other components that depend on it (like UI for the viewer or part info)
            viewerElement = (
                <div id="viewerTools">
                    <UIToolbar viewer={this._viewer}/>
                    <InformationPane viewer={this._viewer}/>
                </div>
            )
        }

        return (
            <div id="hoops-container">
                <div id="canvas"/>
                {viewerElement}
            </div>
        );
    }
}

export default Hoops;

If you would like a sample starting integration of React and HC, be sure to check out our sample integration on our github repo below.

Related Articles