Creating a cross-domain React component, with zoid

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

React is perfect for creating isolated components, on the web.

React components allow you to isolate small, reusable bits of functionality and UI in your app, which you can treat as discrete parts of your code-base, each with its own interface. For example, if I wanted to build a log-in experience, I might build a component like:

<MyLoginComponent prefilledEmail={prefilledEmail} onLogin={onLogin} />

Once my component is built, I don’t have to care how exactly it works or how it’s implemented. I just have to drop in a prefilledEmail and an onLogin callback, and then let the component do it’s thing. It’ll then tell me via the callback when the user has finished logging in.

This is a very functional-ish pattern: the logic and behavior of my component can be totally isolated from the rest of my app, like in a pure-function. It can self-contain its own styles, its own behavior, its own event handlers, and even its own tests.

OK, but you’re preaching to the choir here, we already love React

OK then, let’s talk about some of the things React can’t do.

React components aren’t easily shared or distributed. If I build a component, it’s very easy to drop it into my own site, but if I want to let you use the same component on your site, there are a lot of questions to answer first:

  • Do I have any javascript, css, or anything other code that’s going to conflict with the code already on your page?
  • What if you’re using a different UI framework to me, like Angular or Ember, or you’re not using a framework at all?
  • What if my component does something that it would be insecure to expose to your site? Like, for example, accepting credentials for my domain in an html form, which I don’t want you to be able to snoop on.

So just drop the experience in an iframe?

That definitely solves the security problem, but iframes aren’t exactly flexible.

  • I have to send everything across in the url, or wait until the frame is loaded and send down a postMessage.
  • I have to manually set up postMessage listeners t0 get data back up.
  • Even then, the data I can send back and forth is limited to things I can serialize down to JSON. So callbacks are out of the picture.

So what do we do?

This is where zoid comes in!

With zoid, I can build a React component and make it work entirely cross-domain, in an iframe, without once thinking about setting up DOM elements, postMessage listeners or forcing all of my props into a url’s query string.

If you want to get a really good understanding of what zoid does, I’ve written about it a lot, elsewhere. But for now let’s just give it a go, and turn a same-domain React component into a cross-domain React component!

First let’s build a simple component.

What better than the MyLoginComponent we imagined earlier!

First, let’s write some boilerplate for a simple React component, and render it into our page:

let MyLoginComponent = React.createClass({
    getInitialState() {
return { email: this.props.prefilledEmail };
},
    render() {
    }
});
ReactDOM.render(
<MyLoginComponent />,
document.querySelector('#container')
);

Our component is going to handle one event: a button click on a login button. So let’s set up a handler for that. We’ll introduce an artificial delay, then call that onLogin callback.

let login = () => {
setTimeout(() => {
this.props.onLogin(this.state.email);
}, 2000);
};

Next, we need to return some JSX to render our component, with an email field, a password field, and a button. We also want to pre-populate the email field, if we were passed a prefilledEmail:

return (
<form>
<input
placeholder='email'
defaultValue={ this.state.email }
onChange={ event =>
this.setState({ email: event.target.value })
}
></input>
        <br/>
        <input
placeholder='password' type='password'
onChange={ event =>
this.setState({ password: event.target.value })
}
></input>
        <br/>
         <button
className='btn btn-primary'
onClick={ login }> Log In
</button>
</form>
);

And we’re done! We have something resembling a log-in component. It has no validation or server calls, but you can worry about implementing that later. The important thing is:

  • It populates the email from this.props.prefilledEmail
  • It calls this.props.onLogin() when the button is clicked

You can see the full example code for this here. It has a few extra nice features, like a loading spinner, but fundamentally it’s just a pure React component.

So how do we make it work cross-domain?

OK, so we want other people to be able to render our component from their site. We’re going to have to do a few things to enable this. Most of these should be pretty straightforward.

1. Set up a zoid component

This is where zoid comes in. We need to set up a definition for our component, with the url, and a tag name for our component.

let MyLoginZoidComponent = zoid.create({
    tag: 'my-login-component',
url: 'https://www.my-site.com/login'
});

You can see the full demo code for this here.

This zoid definition, along with the zoid library, need to be loaded in a script tag in both the component page and the parent window where we want to render the component. It’ll provide a way for the iframe to communicate with the parent, and vice-versa.

2. Pass in the props

When we rendered the component earlier, we did:

ReactDOM.render(
<MyLoginComponent />,
document.querySelector('#container')
);

We need to change this slightly. Now, additionally, we need to pass in the props we’ve been passed from the parent window, like so:

ReactDOM.render(
<MyLoginComponent { ...window.xprops } />,
document.querySelector('#container')
);

3. Host it

Now host the React component somewhere on a web-server. We need the html page you just created to be behind some url, so that we can load it in an iframe. In the component definition earlier, we specified url: 'https://www.my-site.com/login' — this url needs to match whatever url you host your component under.

4. Render it!

Now you’re ready to use the component on some different domain, or if you’re sharing it with other people, let them know they can use it on their site!

let App = React.createClass({
    render() {
    let onLogin = (email) =>
alert(`User logged in with email: ${email}!`);
    return (
<div>
<h3>Log in</h3>
                <MyLoginZoidComponent.react
prefilledEmail='foo@bar.com'
onLogin={ onLogin } />
</div>
);
}
});
ReactDOM.render(<App />, document.querySelector('#container'));

Note here we’re using MyLoginComponent.react, which is the React component zoid generates for us (remember, the original React component is going to be living in our iframe, where the parent page can’t directly access or render it).

You can see/run the full code example for rendering the component here.

What if the person I’m sharing with doesn’t have/want React?

No problem! zoid comes with a vanilla javascript way to render the component:

MyLoginZoidComponent.render({
    prefilledEmail: 'foo@bar.com',

onLogin: function() {
alert('User logged in with email: ' + email);
}
}, '#container');

Now anyone can use your React component on their site, without ever realizing they’re using a React component inside the iframe! To them, that’s just an implementation detail — in fact, the fact that they’re using an iframe at all is just an implementation detail.

And we’re done!

Congrats, you’ve just made your component work for anyone who wants to load it into their site!

To make this easier, you can bundle together your component definition with zoid.js and host it somewhere, so whoever wants to use your component can simply do:

<script src="https://www.your-site.com/loginComponent.js"></script>
<script>
MyLoginZoidComponent.render({ ... }, '#container');
</script>

Enjoy!