Introducing zoid — cross-domain, React-like components using iframes

Daniel Brain
8 min readNov 28, 2016

--

This article is part of a series on PayPal’s Cross-Domain Javascript Suite.

This is the second part in a series about how at PayPal, we’re going all-in on our cross-domain javascript component platform. We want to build our products and experiences so they can live seamlessly inside our merchants’ sites, and zoid is the next piece of that puzzle.

Last time, I talked about post-robot, and how it provides a smart way to message between two different frames or windows, and even share functions across the domain barrier. Messaging is arguably the most important part of building embedded web experiences, but even with that problem solved, there’s still a lot of work to be done.

This article will go in depth about zoid, how it goes even further towards helping PayPal build truly cross-domain components, and how it can help you too.

MyLoginComponent.render({    prefilledEmail: 'foo@bar.com',    onLogin: function(email) {
console.log('User logged in with email:', email);
}
}, '#container');

Here I’m rendering a cross-domain, iframe based component with zoid. I’m even passing down data and a callback as props, as if I was rendering a regular react-esque component onto my page. zoid is taking care of all of the cross-domain stuff for me, so I never have to worry about setting up post-message listeners, creating iframes, or any of that other boilerplate.

Why components, anyway?

In a talk I gave earlier this year at Node Summit, I spoke a little about the history of components in the web, starting with reusable css classes, through backbone and bootstrap, through to what we recognize today as best-in-class components using React (or Ember, or even Angular with a little discipline).

Components are great because, in a nutshell, they:

  • Package up some reusable functionality and UI, so all of the logic, styles, interface and even tests for a particular feature can all live in the same place.
  • Are built in a very functional way — they take some input through props, and give some output through callbacks— making front-end code easier to separate, reason about, and test; and allowing us to build isolated components with really clean interfaces.
  • They can be used to contain and manage local state, or combined with a flux/redux style pattern to manage shared state. Either way, we’ve moved well away from storing state in javascript globals, or worse, in the DOM.

So, components are here to stay. Mission accomplished?

Well, not quite. React, Ember, Angular are all great if you want to divide your own app into components. But what if you want to create an experience that you want to distribute; that is, a component that needs to be embedded on other sites on different domains?

Of course, there’s nothing stopping us building a shareable component in our framework of choice and then just doing an npm publish and having everyone else npm install. We can even build our components in different major frameworks, then everyone can use them no matter what stack they’re on!

The sticking point comes when you want to share some functionality, but you need to keep the experience sandboxed on your own domain. This could be for security reasons, or simply to create an isolated experience with its own sandboxed css styles and javascript. For example:

  • Maybe you want to become an oauth provider for 3rd party sites.
  • Maybe you want other sites to be able to embed your site’s videos and get callbacks when the user plays or pauses.
  • Maybe you’re a payments processor like PayPal, and you want to allow users to check-out and make payments on merchants’ sites without sharing credentials or financial details.

What about iframes, popups, post-messaging and so on?

These are exactly the tools we needed. The problem is, they’re slightly clunky interfaces to use:

  • Post messaging doesn’t work consistently across different browsers, especially between parent windows and popups.
  • Post messaging is also fire-and-forget, with no good built-in way to see if a message got through, handle errors, or get responses for our messages.
  • Popups and iframes all have their own quirks, and the security rules and implementation details are different between browsers.
  • Loading iframes after a page render is really noticably slow.
  • Popup blockers make things tricky when you can only open popups on a click event. This is great for blocking ads, but not so much for user-initiated experiences.
  • Passing down data to an iframe involves serializing everything and putting it in query params in the url — or by sending race-condition prone post messages, when you think the iframe has loaded.

Enter zoid

zoid is our attempt to standardize cross-domain components, and to iron out all of the quirks around popups and iframes.

The objectives were:

  • Allow people to build components around embedded, cross-domain experiences (like PayPal Checkout, and the PayPal Checkout button).
  • Support iframes and popups, but abstract away the differences between them.
  • Provide a reliable way to pass down props, including objects and functions/callbacks, which would be transmitted across the cross-domain boundary in the same way that props are passed down through components in React. This way we can do “data down, actions up”, but across the cross-domain boundary.
  • Make the developer experience as seamless as if they were working with same-domain components: no setting up post-message listeners, no constructing urls with serialized data, and no managing child windows.

What we ended up with was zoid.

How do I create these kinds of components?

There are three distinct parts to an zoid:

  1. The component definition. This is shared between the parent page and the child frame.
  2. The component implementation. This is the code that lives inside the child frame, and makes the component work. It has access to all of the props passed down to the component, and it is responsible for calling the correct callbacks when it’s done. Essentially, the entire web-app inside the iframe functions as a component.
  3. The component integration. All we need to do now is actually render the component to our page, and pass down all of the props the component needs to function.

If this sounds familiar, it’s because there’s nothing really groundbreaking about the that pattern. It’s a standard React-like component, with a few differences to account for the fact that it spans between different domains.

How do I build one of these cross-domain components?

Let’s give it a try, and define a login component. Maybe I’m building some cross-domain flow, where the user logs in on my site in an embedded frame, and we notify the parent page when they’re done.

First I’d create the component definition:

var MyLoginComponent = zoid.create({

tag: 'my-login-component',
url: 'http://www.my-site.com/my-login-component'
});

So far so easy. It has a tag name, and a url where the component is going to be rendered.

Now I need to create the component implementation. This is the page which is going to load under the url I set: http://www.my-site.com/my-login-component — this is essentially going to be an entire mini embedded webapp, which lives in the iframe and handles the actual login.

<script src="http://www.my-site.com/my-login-component.js"></script><input id="email" type="text" />
<input id="password" type="password" />
<button id="login">Log In</button>
<script>
var email = document.querySelector('#email');
var pass = document.querySelector('#password');
var button = document.querySelector('#login');
if (window.xprops.prefilledEmail) {
email.value = window.xprops.prefilledEmail;
}
button.addEventListener('click', function() {

var payload = { email: email.value, password: pass.value };

jQuery.post('/api/login', payload, function() {
window.xprops.onLogin(email.value);
});
});
</script>

You’ll notice we’re getting the prefilledEmail from the window.xprops, which are the props the parent passes down, and when the user logs in successfully, we’re calling the onLogin callback we were passed. We didn’t need to set up any post-message listeners or anything!

Now, my work as a developer is done, and I can share the component with anyone. They can then set up a component integration. This is the easiest part: they just render the new component somewhere on their page, with the correct props:

<script src="http://www.my-site.com/my-login-component.js"></script><div id="container"></div><script>
MyLoginComponent.render({
prefilledEmail: 'foo@bar.com', onLogin: function(email) {
console.log('User logged in with email:', email);
}
}, '#container');
</script>

You’ll notice, neither the developer building the component nor the developer using it ever had to:

  • Set up post message listeners.
  • Create an iframe and insert it into the page.
  • Figure out how to pass data down to the child window.
  • Find a way to get a callback back up to the parent.

It’s almost just like setting up a regular same-domain component, without having to deal with any of the glue between the two different domains.

I can even use this cross-domain component natively in React, since zoid automatically sets up bindings for me!

render() {
return (
<MyLoginComponent.react
prefilledEmail='foo@bar.com'
onLogin={onLogin} />
);
}

Cool! Is that all?

There are a bunch of other advanced techniques we can use zoid for, including —

  • Autogenerating React, Angular etc. bindings for components
  • “Rendering up” from an embedded component into the parent page, to break-out of an iframe’s boundaries
  • Pre-rendering cross-domain components before the url loads, to improve their performance and first-render-time
  • Handling error cases which might prevent the component from rendering, and failing gracefully
  • Rendering to popups, not just iframes

— but those I’ll cover in a different article. For now this should give some good reasons for you to consider building your experiences into cross-domain components, using zoid.

If you’re interested in a sneak-preview, here’s the first of the cross-domain components we’ve published for PayPal Checkout. It’s the PayPal Button, and having it exist as a cross-domain component enables us to make it pixel-perfect on every device, and even customize the button for logged-in users.

So this works pretty well for PayPal?

Absolutely. One of PayPal’s main value-props is security. You can pay for something on a merchant’s site with PayPal, without exposing any of your credit card numbers, credentials, or other privileged information.

So at this point, it’s a little bit more tricky than simply saying, “here, we wrote this neat PayPal Checkout component in React, pull our code and you’re good to go!”. That wouldn’t provide any security for customers: if it’s a same-domain component, the buyer is still entering their PayPal credentials or credit card details on a potentially untrusted merchant site.

Traditionally we’ve handled that security problem by simply having the merchant redirect the buyer to PayPal, then redirecting them back to the merchant site when they’re done paying. But that’s a pretty jarring experience for buyers; there are a bunch of redirects, and most customers really just want to pay on the same site they’re purchasing from.

So we have a problem: we want to keep the buyer on the merchant site while they check-out, but we can’t compromise their security in any way. We also want to be able to perform updates and tweaks to our experience, without having the merchant pull in new code. To do that, we needed cross-domain components. And that’s where zoid fits in perfectly.

Thanks,

Daniel | Principal Engineer, PayPal Checkout team

--

--