Thinking of porting a legacy JavaScript library to be a re-usable React component? In this post I describe the process I used to release a React version of JSC3D.

React JSC3D rendering


Rational

I needed a simple, fast 3D mesh ray-tracer in a React 16 project, and none of the few available seemed to work or fit the criteria. So, I decided to port the trusted old JSC3D library to react.

Starting out

I first made the mistake of using create-react-app to start a new component. Don't do this, as it doesn't set up your project to be easily exported as an NPM component.

Instead, I suggest using nwb which has a scaffolding script just for this. The command I ran:

nwb new react-component name-of-component  

Including the legacy code

First I grabbed the legacy script in an unminified format, however concatenated into a single file. To make it easier to debug in the future, I decided to attempt to get things working with as few modifications to the legacy script as possible. Later I may start digging into modernizing the original code and adding unit tests, but for now my main goal was just wrapping it, mostly untouched.

I noticed everything about JSC3D was attached to a JSC3D object. So, to expose it in the Node module system, I added the following to the top:

/* eslint-disable */
var JSC3D = JSC3D || {};  
if (module) module.exports = JSC3D;  

The /* eslint-disable */ disables linting for the file, since the legacy code doesn't follow linting standards at all. The rest simply exposes the JSC3D to whatever requires this as a module.

Integrating with React

Rendering DOM element with reference

In order to mount the legacy code on the page, I needed React to render a <canvas> tag, and maintain a reference to it. I decided to wrap it with a div (to make styling easier) and attach a blank canvas element to this.canvas, as such:

class Jsc3dViewer extends Component {  
    render() {
        return (
            <div className="Jsc3dViewer">
                <canvas                     ref={(canvas) => { this.canvas = canvas; }}>
                </canvas>
            </div>
        );
    }
}

Mounting legacy code with componentDidMount

Then I needed to do is initialize the legacy code when the component mounts. This was done by using the React life-cycle method componentDidMount:

     componentDidMount() {
         this.viewer = new jsc3d.Viewer(this.canvas);
         this._setProps(this.props);
         this.viewer.init();
     }

The _setProps method I created uses the jsc3d API to update the this.viewer with the React component properties. You can see it here.

Preventing React from updating

Using the shouldComponentUpdate life-cycle method, I ensure that the this.viewer stays updated if props change (e.g., if something using this component were to change the model color or something), and also ensure that React.js doesn't overwrite the DOM:

    shouldComponentUpdate(nextProps, nextState) {
        // Set new props
        this._setProps(nextProps);

        if (nextProps.sceneUrl !== this.props.sceneUrl) {
            // Entirely new URL, should re-init
            this.viewer.init();
        }
        return false; // always returns false to prevent
    }

Testing

To test / develop the code I wrote, I used the built-in demo server (with npm run start) that comes with the nwb scaffolding. First I added the media (in this case, stl 3D mesh files) to demo/public, then I edited demo/src/index.js to use my new component:

class Demo extends Component {  
    render() {
        return (
            <div className="wrapper">
                <h1>react-jsc3d Demo</h1>
                <Jsc3dViewer
                    sceneUrl='test-media/trumpet.obj'
                />
            </div>
        );
    }
}

Polishing & Debugging

Proptypes

Once I got things working, I needed to polish everything up. I started by adding PropTypes to my new component so that folks using this will get alerted if they try to pass in an incompatible type. I also wrote a few simple functions to convert more sane / React-friendly data-types into the ones that JSC3D expected (e.g. true and false instead of the strings "on" and "off"). Finally, I wrote documentation in the README.md file so folks will know how to use it -- and wrote this article.

Finally, I wrote some simple tests, set up automated testing with Travis, and released it on NPM!

Debugging

During testing, I noticed it didn't properly define a few variables before use. While browser JS implementations are lenient enough to handle this, this was causing issues when compiling, so I had to specify all of them at the top of the file, like this:

var maxY, minY, maxZ, minZ, maxZ, VBArray;  

For the final product, check out the code on GitHub.

That's it, hopefully this guide might save some time for folks looking to port legacy JS libraries into re-usable NPM packages.