Pattern for providing external React widgets for integration into React apps

More an more web pages are using React. This is also true for service providers which you can use to add a specific feature to your web page. Software that you might add using this way may be a chat application, a bug tracker, a video service and so on.

So we more commonly will face the task to insert an internal React widget into an React web app. Should be an easy task. Well, actually it is not.

That is a problem? Are you kidding?

The trivial approach and why it is likely to fail

Now, assume there is an external React widget providing a chat service which has to be integrated into the React applications of its customers. The customers can modify the appearance and functionality of the widget from a dashboard provided by the service.

React applications are usually driven by Webpack and build from code that is fetched from the NPM-registry.

For providing my widget I could simply take its code, upload it to NPM and tell my customers to fetch it from there. Customers might use it as simple like:

npm install --save myservice-widget

and add it into their page by requiring that module and place it into the correct render method.

So easy? Here are the reasons why this approach fails:

  • The widget you provide often contains some customer specific code. In example many services allow to configure the appearance of their widgets by their dashboard and customers expect to see in their app what they configure here. But as the code is fix in the customers code base you cannot change it any more. A technical solution would be to advise customers to configure the widget via props. But that increases complexity of integrating you what makes you loose customers and limits your abilities to support the them. Alternatively you might load configuration from your servers when the component initializes in the customers shop. But that is bad for performance because you cannot strip away unneeded code at build time any more.
  • You cannot update the widget directly. This means you have to ask your customers to fetch the updates from your NPM repository and release it. You can be sure that customers will fail to do that in time. This would become a big problem in case you want to update your API but cannot get rid of the client code using the old API.
  • The React versions of your widget and the customers application might be incompatible. Technically this might be solved be doing an upgrade on one of both sides. But that may not be possible. In the end you probably will not make get a contract with that customer for technical reasons what will make your boss quite unhappy.
  • You do not want to make customers able to modify your code directly. This can have several reasons, most likely the customer would be able to just remove the code that is used for billing. You do not want that.
  • Strategies between you and your customer for loading images and CSS might collide

Because of the complexity to provide an externally used React component, providers might fall back to just providing some integration code which you are supposed to paste into your page source. That code loads the complete widget and places it onto your page.

This is the standard approach for usual websites anyway.

Benefits of trying it nonetheless

Integrating via script tag works, so you might ask why bother about this. But the standard integration means for React apps that they inject a second React app. Both contain their own version of React what is not very memory efficient and makes your widget load slower than it could do

Here is why you might bother providing a native React integration of your React widget into React apps:

  • Customers can fully control themselves where to render your widget at what time. They just place it in the correct render method as they are used to do.
  • Customers can easily pass their own configuration via props in case your code supports them
  • You might share the same React instance to save memory and loading time
  • Do not care about the problems that would appear otherwise. Customers might become to properly unload and reload your widget on page navigation in singe page application if you just provide the standard integration as for normal pages. Customers having to do something right means problems for you

Proposal

I faced exactly those problems having to integrate videos into all kinds of webpages and React apps at DemoUp.

My solution is to build a small wrapper component with generic code that is shared via NPM. Customers are supposed to install that component and build it into their apps. The component is responsible for loading the custom code from DemoUp servers and pass the target DOM element and props onto that component. The component is small and not intended to receive many updates.


import {Component} from "react";
var CrossCustomEvent = require('customevent');
var scriptLoader = require("load-script");
module.exports = function(identifier) {
/* Customers pass some kind of identifier to this function.
* It might be a user name or an access key.
* Something that identifies the location of the script code of this customer
*/
class Wrapper extends Component {
componentDidMount = () => {
var propsToPass = Object.assign({}, this.props);
/*
* The loaded component code is accessible by a unique global variable
* As soon as that code is loaded this component will call it
* and pass its DOM element and the props to it
*/
if (window.MyWidgetRenderer) {
window.MyWidgetRenderer(this.$container, propsToPass);
} else {
scriptLoader(`path of customer specific script, might be derived from ${identifier}`, () => {
this.forceUpdate(() => {
window.MyWidgetRenderer && window.MyWidgetRenderer(this.$container, propsToPass);
});
});
}
}
componentWillUnmount = () => {
/*
* Problem is that the widget will not automatically detach if this wrapper is unmounted
* I solved the problem by common event on which the widget listens and cleans itself up
*/
document.dispatchEvent(new CrossCustomEvent('widget.unmounted'));
}
setRef = (node) => { this.$container = node; }
render: () => {
var props = this.props;
return <div {props.nodeAttrs} ref={this.setRef}>
{
(window && window.MyWidgetRenderer) ? null : props.children
}
</div>;
}
};
Wrapper.defaultProps = {
nodeAttrs: []
};
};

On the other side you need some wrapper code around the widget you want to be rendered this way:


import { render } from "react";
import MyWidget from "./components/mywidget.jsx";
window.MyWidgetRenderer = function(node, props) {
render(<MyWidget {props} />, node);
};

And that’s it!

Customers can just install your ComponentLoader from your public NPM repository, provide an identifier and mount the component. Problem solved.

I did not try to use the React instance of the customer app for rendering the widget, because my widget is written in Preact. It is likely possible to pass React as a parameter to the RenderWrapper.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s