Cross-Domain Apps with Create-React-App and Zoid

6 minute read

Hello there! Today I will share a tutorial on how to create cross-domain React Apps.

Have you every thought it would be cool if you could package a particular React (or Vanilla JS any other framework) component and display it on a different website, whether or not it was in the same framework?

Well, Paypal definitely thought of that a long time ago, because they wanted to put their Paypal button on as many merchant sites as possible! paypal

They experienced a myriad of problems, though, which are well-documented in this blog post by the creator of Zoid, Daniel Brain

If you have a similar task to accomplish (i.e. easily hosting cross-domain components in an I-frame), chances are you’ve come across the Zoid documentation, and their “Login Button” demos.

My issue with these simple demos is that they don’t actually show how you would use React normally with Zoid. Just this contrived jsx, <script>- using, babel-ified super simple example.

Admittedly, I likely don’t have such a strong grasp of React compared with the paypal people, so I spent a long time puzzling over

what about import _ from '_' instead of <script>??

How do I make sure the correct component definitions load at the right time?

etc.

So I answered some of these questions for myself and thought I’d share 😊

Let’s Begin!

Note source code for this tutorial is available here

We will start by creating two seperately hosted React Applications using Create-React-App

So, in a common directory, run the following:

npx create-react-app parent-app;
cd parent-app;
npm i zoid

Of course this will take a while to get the app up and going (but how can you use React without taking a long time to get up and going?). To make the child component is the same process. From the common directory:

npx create-react-app child-app;
cd child-app;
npm i zoid

Okay, next thing to do would be to edit the start script of the child app so it’s not running on the same port. Open child-app/package.json and change the following line:

"scripts": {
    "start": "react-scripts start",
    ...

to

"scripts": {
    "start": "PORT=2000 react-scripts start",
    "start": "set PORT=2000 && react-scripts start",// **if Windows:**
    ...

At this point you can run npm start from both app directories and get two simulatneously hosted (“cross domain”) apps. Now is where the fun begins!

(side note I’d recommend changing the background color or something in one of the frames so you don’t confuse yourself switching back and forth)

Editing the Apps

I want to showcase the canonical example of React component communication. Namely:

props down, events up

We want to pass two things from parent to child: 1) a value, name which is passed as a prop 2) a function, passDownFunc which when called from the child has an effect on the parent.

in child-app/src/App.js lets add the following lines:

import React, {useState} from 'react';

function App(props){
    let [myWord, changeMyWord] = useState('')
    ...
    return(
        ...
       <p>
         Hello Im a beautiful widget!
        My name is <code>{props.name || "undefined"}</code>
        </p> 

        <input
        value={myWord}
        onChange={e=> changeMyWord(e.target.value)}
        />
        <button
        conClick={()=> props.passDownFunc(myWord)}>
        Pass this word up to parent
        </button>
    )
}

From the above, we know what 1) we will get a prop called name that we will render and a prop called passDownFunc that we will use to do ??? depending on how the parent defines that function (but at least we know that it’ll take a string as an argument).

The next step is to Zoid-ify this component/app that we’ve made - which we ill call MyWidget

Open a new file in child-app/src and call it widget.js:

import * as zoid from "zoid/dist/zoid.frameworks";

let MyWidget = zoid.create({
    tag: 'my-widget',
    url: 'http://localhost:2000/index.html'
})

console.log('yo! have loaded mywidget from child: ')
console.log(MyWidget)

export default MyWidget

The console logs are optional, but allow you to see the type of MyWidget. Two very important points to note: 1) If you’re going to use React you must import from zoid/dist/zoid.frameworks. See this Github issue for more deets. 2) remember to put /index.html in your url. otherwise it fails!

Now, as this file isn’t being used anywhere, we’ll have to import it into our app. And the place we import it is index.js!

...
import MyWidget from './widget';
...

Yes, there’s a good chance you’ll get a liniting error because MyWidget isn’t used anywhere else in the file. That’s okay.

Likewise, this widget.js is the “bridge” between our two apps, so we’ll copy and paste it into parent-app/src

Important Note: I know it’s redic to C&P in a real-life situation. There are ways to import a <script> tag into a React Component, which I haven’t tried, but you are more than welcome to 😁 I think it won’t be difficult, but there may be some asynchronous/ lifecycle -type debugging to do (which is why I didn’t try lol)

Anyway, whether you import or load the script via its src, the point is you want the zoid-created MyWidget to be available in your parent-app/src/App.js:

...
import MyWidget from "./widget";
...
let MyReactWidget = MyWidget.driver('react',{
  React: React,
  ReactDOM: ReactDOM
})
...
function App(props){
    return(
        ...
        <MyReactWidget name="foobar" passDownFunc={()=> console.log("iFrame did something")}
    )
}

That is the crux of how you include a zoid component in your CRA app. After that I just added a little bit of fun stuff in App.js using React Hooks: (oh full disclosure I did try to pass the child name prop via an input element, but I was coming across problems with re-rendering the iframe. Dunno if that’s a legit issue with Zoid, or if I’m just a n00b. So I did the prompt trick. )

function App(props) {
  const [myThings, setMyThings] = useState(["burger", "fries"])
  let [widgetName, setWidgetName] = useState('')
  
  useEffect(()=> {setWidgetName(window.prompt("What's the child's name?"))}, [])

  const widgetFunc = word => setMyThings(myThings => [word, ...myThings])
  
  return (
  
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          These are my things: {myThings.map((thing, idx) => {
            let randColor = Math.floor(Math.random()*16777215).toString(16);
            
            return (<span 
            key={`thing${idx}`} 
            style={{
              color:`#${randColor}`,
              fontWeight: "bold"
            }}
            className="randomThing">{thing}; </span>)
        })}.
        </p>
        {widgetName && <MyReactWidget name={widgetName} passDownFunc={widgetFunc} />}
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
      
    </div>
  );
}

Summary:

In order to go from a Create-React-App Parent and Child to a CRA Parent and IFrame Child, the steps are as follows:

  1. create the child app, making note of what props (including functions) it will get from the parent.
  2. Define the zoid component in your child’s src and import that file into your index.js to make sure the script loads.
    • make sure to import * as zoid from "zoid/dist/zoid.frameworks"
  3. Import the same exact definition script into your Parent App
  4. Use the React driver to turn the zoid component into a React Component
  5. Render your React-ified component in your Parent App and it’ll be an IFrame!

Considerations

  • There are a lot more advanced features in Zoid, not least of which is customizing size/shape/etc. I’ve avoided mentioning those things b/c my focus is getting it to work with CRA.
  • If your parent or child is non-React, it will still work (in fact there are probably less steps 😊)

Hope this helped. If you have any questions please let me know! And a big thank you to @bluepnume and Facebook for their awesome work.