DEV Community

Ryan Baker
Ryan Baker

Posted on

How to Build an Embedded Widget with React & Redux

In this article I’m going to go over how to set up a React project if you wanted to build something like Drift. At Drift I often work on the chat widget. Outside of work, I recently open sourced a side project called Weasl. Weasl allows you to add pain free user login to your website. These are complex projects that leverage React and Redux to manage rendering and application state. In this blog post, I’m going to do a high level walkthrough of the architecture, client side API, and management of loading your embedded widget on a client’s page.

A person focusing at a laptop as the sun rises

Anatomy of the Embed

The embed is made up of 3 parts:

  1. The snippet — this is what the user pastes onto their website. This code is primarily responsible for loading the shim
  2. The shim — this is the code that runs on the client’s website and talks to your embedded widget
  3. The widget — this is iframed (for security reasons) webapp that receives messages from and sends messages to the parent window. This is the only thing talking to your server API.

I’ll go over all of these in more detail.

The snippet

My side project’s (Weasl) snippet looks like this:

(function(window, document) {
if (window.weasl) {
  console.error('Weasl embed already included');
  return;
}
window.weasl = {},
m = ['init', 'login', 'signup', 'setAttribute', 'getCurrentUser', 'logout', 'debug'];
window.weasl._c = [];
m.forEach(me => window.weasl[me] = function() {
  window.weasl._c.push([me, arguments])
});
var elt = document.createElement('script');
elt.type = "text/javascript";
elt.async = true;
elt.src = "https://js.weasl.in/embed/shim.js";
var before = document.getElementsByTagName('script')[0];         before.parentNode.insertBefore(elt, before);
})(window, document, undefined);
weasl.init('MY CLIENT ID HERE');
Enter fullscreen mode Exit fullscreen mode

This seems daunting to look at, so let’s deconstruct it.

First, we want our snippet to run immediately when the page loads. This means we need an IIFE.

(function(){})();
Enter fullscreen mode Exit fullscreen mode

We want to declare our global (window) variable that will be used to load the widget and provide the JS SDK.

(function(window) {
if (window.weasl) {
  console.error('Weasl embed already included');
  return;
}
window.weasl = {};
})(window);
Enter fullscreen mode Exit fullscreen mode

Next, we want to add the shim’s script tag to the page:

(function(window, document) {
if (window.weasl) {
  console.error('Weasl embed already included');
  return;
}
window.weasl = {};
var elt = document.createElement('script');
elt.type = "text/javascript";
elt.async = true;
elt.src = "https://js.weasl.in/embed/shim.js";
var before = document.getElementsByTagName('script')[0];         before.parentNode.insertBefore(elt, before);
})(window, document);
Enter fullscreen mode Exit fullscreen mode

Next, we want to keep track of the library calls that happen before our script is loaded, so we can make sure to call them after it’s been loaded. I’ve renamed a couple variables for clarity:

(function(window, document) {
if (window.weasl) {
  console.error('Weasl embed already included');
  return;
}
window.weasl = {},
methods = ['init', 'login', 'signup', 'setAttribute', 'getCurrentUser', 'logout', 'debug'];
window.weasl._beforeLoadCallQueue = [];
methods.forEach(methodName => {
  window.weasl[methodName] = function() {
    window.weasl._beforeLoadCallQueue.push([methodName, arguments]);
  }
});
var elt = document.createElement('script');
elt.type = "text/javascript";
elt.async = true;
elt.src = "https://js.weasl.in/embed/shim.js";
var before = document.getElementsByTagName('script')[0];         before.parentNode.insertBefore(elt, before);
})(window, document);
Enter fullscreen mode Exit fullscreen mode

And finally, we want to make sure we call the initialize function of our SDK.

(function(window, document) {
if (window.weasl) {
  console.error('Weasl embed already included');
  return;
}
window.weasl = {},
methods = ['init', 'login', 'signup', 'setAttribute', 'getCurrentUser', 'logout', 'debug'];
window.weasl._beforeLoadCallQueue = [];
methods.forEach(methodName => {
  window.weasl[methodName] = function() {
    window.weasl._beforeLoadCallQueue.push([methodName, arguments]);
}
});
var elt = document.createElement('script');
elt.type = "text/javascript";
elt.async = true;
elt.src = "https://js.weasl.in/embed/shim.js";
var before = document.getElementsByTagName('script')[0];         before.parentNode.insertBefore(elt, before);
})(window, document, undefined);
weasl.init('MY CLIENT ID HERE');
Enter fullscreen mode Exit fullscreen mode

That’s about it for the anatomy of the snippet. Next up is

The Shim

The job of the shim is to figuratively “shim” your code into the client’s website. The shim and the snippet both run on the client’s website. They should both be very lightweight and minimal so they can load as fast as possible (or allow the client to defer loading, if they want).

The shim is more complex, so I won’t include all of the code in this blog post. You can find Weasl’s shim in a single open source file, though. The basic idea is this:

// see if init() was called
// if so, create and add the iframe to the page
// tell the iframed webapp to start loading
// listen for iframe to tell us it's done
// add the real methods to the window SDK
// run the queued up calls that were made before we were ready
Enter fullscreen mode Exit fullscreen mode

and a more fleshed out version of the code

class WeaslShim {
  init = (clientId) => {
    this.clientId = clientId;
    this.mountIframe();
  }
  mountIframe = () => {
    const iframe = document.createElement('iframe');
    iframe.onload = () => {
      this.iframe.contentWindow.postMessage({
        type: 'INIT_IFRAME',
        value: { clientId: this.clientId }
      }) '*');
    };
    // IFRAME_URL is the url for the index HTML page of our
    // plain old react/redux webapp
    iframe.src = IFRAME_URL;
    iframe.crossorigin = "anonymous";
    this.iframe = iframe;
    window.addEventListener("message", this.receiveMessage, false);
    const wrapper = document.createElement('div');
    wrapper.appendChild(this.iframe);
    document.body.appendChild(wrapper);
  }
  receiveMessage = (event) => {
    // this is where we handle when our widget sends us a message
    if(!!event && !!event.data && !!event.data.type) {
      switch(event.data.type) {
        case 'IFRAME_LOAD_DONE':
          this.handleWidgetLoaded();
          break;
      }
    }
  }
  handleBootstrapDone = () => {
    const weaslApi = window.weasl;
    // these methods aren't filled out here, but you can
    // imagine what they might do
    weaslApi.login = this.login;
    weaslApi.signup = this.signup;
    weaslApi.getCurrentUser = this.getCurrentUser;
    weaslApi.setAttribute = this.setAttribute;
    weaslApi.logout = this.logout;
    weaslApi.debug = this.debug;
    weaslApi._c = window.weasl._c;
    this.runPriorCalls();
    window.weasl = weaslApi;
  }
  runPriorCalls = () => {
    window.weasl._c.forEach(([method, args]) => {
      this[method].apply(this, args);
    });
  }
}
export default ((window) => {
  const stubSdk = window.weasl;

  // _c is the real name for the _beforeLoadCallQueue
  const initCall = stubSdk._c.filter(call => call[0] === 'init');
  const shim = new WeaslShim();
  stubSdk.init = shim.init;
  initCall && shim.init(initCall[1]);
})(global)
Enter fullscreen mode Exit fullscreen mode

It’s important to note that the IFRAME_URL is the URL for your react/redux webapp.

This is a lot more complex than the snippet, but the basic idea is all there. Whenever the widget needs to talk to the shim (for example, if it needed to change the size of the div container) then it could send a postMessage to do just that. Last, we’ll finally get to talk about

The Widget

Since we properly encapsulated the snippet and the shim, we get to write our widget as a fully fledged react/redux app!

If you take a look at the code, you can see that the Weasl widget is written just like any other react/redux app. The biggest change is that the top level App component will now listen for post messages and dispatch them into redux:

class WeaslEmbed extends Component {
  constructor(props) {
    super(props)
    window.addEventListener("message", this.receiveMessage, false);
  }
  receiveMessage = (event) => {
    if(this.isWeaslEvent(event)) {
      this.props.dispatch({ type: event.data.type, payload: event.data.value });
    }
  }
  isWeaslEvent = (event) => {
    return !!event && event.data && event.data.type && event.data.type in SharedEventTypes;
  }
  render() {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

And as a result of certain actions, we can use middleware to listen to the actions and send a post message back to the parent window.

In Conclusion

We’ve gone over how to create an embeddable widget and all its parts. The things that I’ve left off here, but are also important to consider are things like:

  1. Generating a unique client ID for each user
  2. Making sure you keep yourself secure with a CSP and limiting the iframe postmessage domains
  3. Verifying the page the embed was loaded on is whitelisted
  4. Locking down your API endpoints by restricting access
  5. There are a lot more things to consider when creating a widget that runs on tons of websites, but this is a good introduction to get you started.

Happy Coding 💻

Top comments (0)