Porting a JS Library to a React Component

Guest post from Kloudless helps improve React interoperability

With the recent growth in front-end technologies, React has become one of the most popular frameworks. We’ve also seen interoperability among these frameworks become a high priority, especially when including vanilla JavaScript.

Therefore, it is pivotal for organizations to focus on interoperability for React developers. In this article, we introduce how to port a JS library to a reusable React component as well as a Higher-Order-Component (HOC). We also cover any obstacles we encountered along the way and offer helpful tips on how we went about resolving them.

We’ve simplified the example code in this article for the sake of readability. You can also visit the GitHub page to get the full sample code.

A Brief Introduction to JS libraries

Most vanilla JavaScript libraries are included via a <script> tag somewhere in the HTML, whether in the head or at the bottom of the page. Often times, the JavaScript library functions as a widget that a user will launch on the page.

In this example, the File Explorer JS library launches a UI widget that allows users to browse and select files and folders from cloud storage services.

We configure this JS library by instantiating an instance of the explorer with configuration options. This JS library exposes a choose() method, and it can be called programmatically to launch the JS widget.

const options = {
  app_id: 
};
const explorer = window.Kloudless.explorer(options);
buttonElement.onclick = function() {
  // call this method whenever you want to launch Chooser
  explorer.choose();
};

Let’s Start

We chose to use React 15 for this project because it was the most stable version at the time of development. However, React does a very good job of maintaining stable versions and backward compatibility.

As a developer, we do not want to rewrite the entire JS library since the functionalities will continue to be used by existing users and customers. Therefore, we chose to add a React wrapper and simply reuse the core JS code.

Many vanilla JS libraries rely on user input and interaction; therefore, we need React to render a button to maintain the same functionality. We include this by using the lifecycle event componentDidMount(), and we bind a click event handler to the button. Now, we can call exposed JS methods like choose() to launch the JS widget whenever the button is clicked.

In addition, we can use componentWillReceiveProps() to re-initialize the instance of the JS library to ensure that changes to props will be propagated.

Since React and other frameworks no longer import from a script tag in the HTML, we expose the JS library with a global variable to import the JS library as an ES6 module.

class Chooser extends React.Component {
  constructor(props) {
    super(props);
    this.explorer = null;
    this.onClick = this.onClick.bind(this);
  }
  componentDidMount() {
    // initialize explorer
    this.explorer = window.Kloudless.explorer(this.props.options);
  }
  componentWillReceiveProps(nextProps) {
    // re-initialize the explorer when options changing
    if (nextProps.options !== this.props.options) {
      this.explorer = window.Kloudless.explorer(nextProps.options);
    }
  }
  onClick() {
    // call choose() to launch the Chooser
    this.explorer.choose();
  }
  render() {
    return ;
  }
}

So far so good! However, some JS libraries use iframes to customize the look and feel of the JS widget. In this case, we noticed an issue with iframes and a possible memory leak.

When working with JS widgets, diving into the source code is a necessity when porting over to React. Sometimes minor modifications to the JS library are still required.

var initialize_frame = function (options, elementId) {
  var frame = document.createElement('iframe');
  // configure frame
  var body = document.getElementsByTagName("body")[0];
  if (elementId) {
    var el = document.getElementById(elementId);
    el.appendChild(frame);
  } else {
    body.appendChild(frame);
  }
}
 
Kloudless.explorer = function (options) {
  initialize_frame(...);
}

We need a cleanup mechanism to remove the iframe by adding a destroy() method to the JS library.

Kloudless.explorer.destroy = function () {
  frame.parentNode.removeChild(frame);
};

We can then call destroy() to properly remove the iframe.

  • componentWillReceiveProps(): calls before the new explorer is initialized.
  • componentWillUnmount(): calls before the component un-mounts

The final React code is as follows:

class Chooser extends React.Component {
  constructor(props) {
    super(props);
    this.explorer = null;
    this.onClick = this.onClick.bind(this);
  }
  componentDidMount() {
    // initialize explorer
    this.explorer = window.Kloudless.explorer(this.props.options);
  }
  componentWillReceiveProps(nextProps) {
    // re-initialize the explorer when options changing
    if (nextProps.options !== this.props.options) {
      this.explorer.destroy();
      this.explorer = window.Kloudless.explorer(nextProps.options);
    }
  }
  componentWillUnmount() {
    this.explorer.destroy();
  }
  onClick() {
    // call choose() to launch the Chooser
    this.explorer.choose();
  }
  render() {
    return ;
  }
}

Advanced: Higher-Order Component

Although this React component is usable, we want to simplify the entire process and modify as little code as possible. For developers who have already implemented their own button components, a higher-order-component (HOC) may be an easier way to enhance their components to handle user events. This will also prevent having to rewrite the entire component logic.

Most React developers have probably used a HOC before, such as Redux’s connect(). It is essentially a function that takes a component and wraps it into a new component. (See React Documentation for more details.) By adding support for a HOC, developers can transform their custom components into a JS library component more easily.

// create chooser component
const chooser = createChooser(customComponent);
 
// usage

Our HOC should fulfill the following requirements:

  • Add a transparent layer above the wrapped component to manage the JS library instance.
  • It should not impact the original use of the wrapped component. That is, besides the JS library’s specified props, all other props should bypass to the wrapped component. Also, the wrapped component’s static methods should be copied to the new component.
  • We need to hack the click event handler to call choose().
  • If the wrapped component doesn’t have a click event handler or its name is not onClick, just ensure that the method is properly called in the wrapped component. For example, when the button is clicked.
    The final HOC createChooser code is as follows:
 
import hoistNonReactStatics from 'hoist-non-react-statics';
 
const createChooser = (WrappedComponent) => {
  class Wrapper extends React.Component {
    constructor(props) {
      super(props);
      this.explorer = null;
      this.onWrappedCompClick = this.onWrappedCompClick.bind(this);
    }
    componentDidMount() {
      const { options } = this.props;
      this.explorer = window.Kloudless.explorer(options);
    }
    componentWillReceiveProps(nextProps) {
      const { options } = this.props;
      if (nextProps.options !== options) {
        this.explorer.destroy();
        this.explorer = window.Kloudless.explorer(options);
      }
    }
    componentWillUnmount() {
      this.explorer.destroy();
    }
    onWrappedCompClick(...args) {
      const { onClick = () => { } } = this.props;
      onClick(...args);
      this.explorer.choose();
    }
    render() {
      // do not pass options and event handler to the wrapped component
      const { options, onClick, ...restProps } = this.props;
      return (
        
      );
    }
  }
 
  // set display name for easy debugging
  Wrapper.displayName = (`createChooser(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`);
 
  // use hoist-non-react-statics to copy non-react static methods
  hoistNonReactStatics(Wrapper, WrappedComponent);
 
  return Wrapper;
};

Conclusion

In this article, we covered how we port our JS library, File Explorer, into a reusable React component. We have also provided an option to wrap your custom component in a Higher Order Component. React’s lifecycle methods make the process of porting a JS library to React much easier through the use of componentDidMount, componentWillUnmount, and componentWillReceiveProps. One final note is to be careful about side effects, such as manipulating global variables, event listeners, or the DOM tree. Make sure they work independently of multiple components and ensure that components are cleaned up when unmounting. Follow these tips and you should have little problem porting over your own libraries.