Cross-Domain Apps with Create-React-App and Zoid
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!
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:
- create the child app, making note of what props (including functions) it will get from the parent.
- Define the zoid component
in your child’s
src
and import that file into yourindex.js
to make sure the script loads.- make sure to
import * as zoid from "zoid/dist/zoid.frameworks"
- make sure to
- Import the same exact definition script into your Parent App
- Use the React driver to turn the zoid component into a React Component
- 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.