Rendering in an iframe in a React app

ยท

4 min read

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: codesandbox.io/s/render-in-iframe-1-eq4tn?f..

image.png It rendered nothing ๐Ÿ˜ญ

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:

  1. Get the ref of the iframe by using useState rather than useRef because useState causes re-render when the ref is set.
  2. We get the body of the iframe document and traverse with optional chaining because ref might not be readily available.
  3. 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: codesandbox.io/s/render-in-iframe-2-qgy4i?f..

image.png Yay! it works ๐Ÿ˜ƒ

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.

ย