AUTO1 Group

Embeddable Lightweight React Widgets

By Kareem Elbahrawy

Kareem is Senior Frontend Engineer at AUTO1 Group

< Back to list
Social Dec 11 2018

"We need to re-write EVERYTHING!"... It's understandable that a huge technical debt is created during the first period of building a company app, a lot of business needs, a lot of deadlines and smaller teams. And for webapps where a proper frontend solution was not considered, most of this technical debt falls on frontend and by time it becomes harder and harder to refactor till it reaches a point where the right way to refactor it is to re-write it. But that's not an option sometimes, isn't it?

As an alternative, This article will introduce two approaches to integrate lightweight react apps (we will call them widgets) that can be embedded in any project.

  • Traditional Approach
  • Standalone Approach

React Widgets: Traditional Approach

React widgets implemented directly in your codebase.

Create a similar folder structure to this:

.
├── server
    └── ....
├── frontend
    ├── widgets
        ├── login.js
        └── subscribe.js
    └── components
        ├── Login.js
        ├── Subscribe.js
        └── ....
├── dist #ignored
    ├── widget_login_bundle.js
    └── widget_subscribe_bundle.js
├── webpack.config.js
└── package.json

/frontend/widgets directory will contain entry points for all the widgets, each file inside should render a react app in a corresponding DOM element id

For example /frontend/widgets/login.js

import React from 'react';
import ReactDOM from 'react-dom';
import Login from '../components/Login';

ReactDOM.render(
    <Login />,
    document.getElementById('widget:login'), // <-- NOTE the element id
);

webpack.config.js will look like this

module.exports = {
    entry: [
      './frontend/widgets/login.js',
      './frontend/widgets/subscribe.js',
    ],
    output: {
        filename: 'widget_[name]_bundle.js',
    }
    ...
}

Now running webpack from the project folder will create two bundle files widget_login_bundle.js and widget_subscribe_bundle.js in the dist folder.

Now you can use a widget in one of your views like this

...
<div id="widget:login"></div>
<script defer src="/dist/widget_login_bundle.js" />
...

Pros

  • Easy to implement and straightforward.
  • Doesn't need any further backend integration to make it work (except for SSR).

Cons

  • Depending on your backend, it might not be an easy task to server side render (SSR) widgets with this approach.
  • You will need to configure your deployment process to run webpack.
  • The frontend development and deployment processes are tightly coupled to the backend.

React Widgets: Standalone Approach

In this approach we will keep all frontend in a separate repository and have an npm script to generate an html and bundle js file for each widget and upload them to a CMS.

But first, A new database model need to be introduced that will hold the widget data (name and html) and there should be a way to upload assets to a content server.

For example you may expose two web service APIs similar to this:

  • POST example.com/api/widget/save-or-update (Save or upload a widget entry in the database)
  • POST example.com/api/content-server/upload (Upload an asset file to content server)

The folder structure will be similar to the previous approach but this time in a separate repository:

.
├── frontend # Same as before
├── dist # Same as before
└── scripts
    ├── deployWidget.js
    └── ....
├── webpack.config.js
└── package.json

/scripts/deployWidget.js is a nodejs script that expects a --widget argument and will do the following:

  • Upload this widget bundle file to a content server
  • Create or update the widget entry html value in the database.
const path = require('path');
const argv = require('yargs').argv;
const uploadAssetToContentServer = require('./uploadAssetToContentServer');
const createOrUpdateWidgetEntry = require('./createOrUpdateWidgetEntry');

const deployWidget = async ({ widget }) => {
    const bundleFile = path.join(
        process.cwd(),
        'dist',
        `widget_${widget}_bundle.js`
    );
    const uploadedUrl = await uploadAssetToContentServer(bundleFile);
    const html = `
        <div id="widget:${widget}"></div>
        <script defer src="${uploadedUrl}"></script>
    `;
    await createOrUpdateWidgetEntry({
        name: widget,
        html,
    });
    console.log(`widget ${widget} deployed successfully`);
};

deployWidget({
    widget: argv.widget,
}).catch(console.error);

NOTE: I encapsulated the logic to upload the bundle file and save widget entry in uploadAssetToContentServer.js and createOrUpdateWidgetEntry.js respectively, which you will need to implement.

In package.json

...
"scripts": {
    ....
    "deploy-widget": "webpack && ./scripts/deployWidget"
}

And now to deploy the login widget:

npm run deploy-widget -- --widget login

Going back to the backend repository, You can render a specific widget using the html value saved in DB, for example

...
{{ getWidgetHtmlFromDB('login') | raw }}
...

Pros

  • You have full control over what to put in the widget html so it's quite easy to enable SSR, critical css, ...etc.
  • The development and deployment processes for frontend is totally separated from backend (happier developers).
  • Deployment process is much faster which will make daily AB tests more realistic.
  • More suited for larger teams where separation between frontend and backend is more clear.

Cons

  • Remember the saying "with great flexibility comes great responsibility"? The widget deployment process is merely updating an entry in DB, if you didn't build proper versioning around this feature or you allowed developers to push directly to production DB then disasters might happen.
  • SSR is quite hacky with dynamic data... doable but hacky.
  • Needs a lot of work on the backend side to integrate and maintain this feature properly in the CMS.

What's next

  • Having one vendor file that's shared between all widgets. @see webpack DllPlugin
  • For Standalone Approach

    • Add Server Side Rendering and critical css in deployWidget.js script.
    • Add versioning for the widget entry in CMS i.e. each widget deployment should be associated with a version number that you can rollback in CMS.
    • Only allow the script deployWidget.js to deploy widgets to QA env. and create the scary-red "Move to production" button in QA CMS to just copy the entry from QA DB to Production DB.
Stories you might like:
SocialDec 13 2018
By Paul Hofmann

In this article I want to highlight my personal impressions and key takeaways from being at the...

SocialJul 2 2018
By Szymon Pasko

New Tech Center in Poland

SocialJun 20 2018
By Davide Debernardi

Event diary of our 2018 Spring Hackathon, UN<->DOCK