Sometimes you want to render a tree of components within an iframe
. You may want to do this because you may be creating something like CodeSandbox that runs other people's code or developing a Storybook-kinda library to design & test components in isolation or you are building a visual editor for designing web apps likes us.
Whatever the reason may be, you would use an iframe for the following two features that iframe offers:
You want to isolate the styles between the parent document and the iframe document. Since the styles are inherited by the children because of the cascading nature of CSS. CSS stands for Cascading Style Sheets after all.
You want to sandbox JS used in an iframe for security. For example, the content in the iframe loads a script from an untrusted location.
As an initial effort to render a component within an iframe, I went ahead by writing the following straight-forward code:
function RenderingInIFrame() {
return (
<iframe>
<MyComponent />
</iframe>
);
}
Codesandbox: https://codesandbox.io/s/render-in-iframe-1-eq4tn?file=/src/App.js
Boy, to my surprise this did not work because iframe does not expect children. iframe is a nested full-fledged document so adding content to it would not be this simple.
So, I explored the ways that I could do it. The normal way to do it is to add the src
attribute with the URL of the app that will run in it. Essentially you would be creating two separate React apps that run in different contexts. This makes it very difficult to share the state between these apps because JS is sandboxed in iframe. You would have to devise complicated setups to share the state and I am not going to explore that here.
Let us try another way that is simple enough, does not require two apps, and can share logic fairly easily. We will need to find a way to render the children within the body of the iframe.
That is where ReactPortal
comes to the rescue. Portals are a way to render something virtually at one location and actually render at another. If you want to know more about Portals, you may read them here.
So, we need to create logic where we virtually render at the iframe's children but it renders within iframe's document body. So, here is my hack:
function IFrame({ children }) {
const [ref, setRef] = useState();
const container = ref?.contentWindow?.document?.body;
return (
<iframe ref={setRef}>
{container && createPortal(children, container)}
</iframe>
);
}
Let me explain line-by-line. Here we created an IFrame wrapper that will render its children in an actual iframe. How we do it is:
- Get the
ref
of the iframe by usinguseState
rather thanuseRef
because useState causes re-render when the ref is set. - We get the
body
of the iframe document and traverse with optional chaining because ref might not be readily available. - Then we create a portal that renders the children within the body of the iframe.
Now, let us try running the same example while using the newly created IFrame:
function RenderingInIFrame() {
return (
<IFrame>
<MyComponent />
</IFrame>
);
}
Codesandbox: https://codesandbox.io/s/render-in-iframe-2-qgy4i?file=/src/App.js
You can pass in any children to this IFrame and it will work flawlessly. It works this way because the children are rendered in the context of the main app and DOM is updated via Portals. The event handlers on the components within the IFrame also work without issues granted that they are React's synthetic events. For ex:
<IFrame>
<button onClick={increment}>Increment</button>
</IFrame>
Even though JS and events is not shared between the iframe and the main document, these event handlers work because React emulates the event mechanism using its own synthetic events. So any event that is caught by React will bubble up the tree. For the first time, I have found the why? behind React's own events and they are awesome.
Final Notes
Though you may now know how to render within an iframe by writing an IFrame wrapper from scratch, there is already a library that does this for you and has more features & use-cases built in called react-frame-component.
Hope this has helped you in some way.
Top comments (6)
I think you could use contentDocument to shorten the code
Yeah, you are right. It also works with
ref?.contentDocument?.body
. Thanks you for sharing.Thanks for the great writeup, I noticed styles are not being applied. How can that be fixed?
Hey man did u ever figure out how to fix the styling
How can I inject react components into an iframe which registers itself to the Dom element I provide. In that case I don't own the iframe, but want to inject into it.
Very useful. Thanks!