69

I am trying to set the content of an iframe in a React component but I am not able to do it. I have a component that contains a function which has to be called when the iframe finishes loading. In that function I am setting the content but it doesn't seem like the onload function is called at all. I am testing it in the Chrome browser. I am trying the following:

var MyIframe = React.createClass({
    componentDidMount : function(){
        var iframe = this.refs.iframe.getDOMNode();
        if(iframe.attachEvent){
            iframe.attacheEvent("onload", this.props.onLoad);
        }else{
            iframe.onload = this.props.onLoad;
        }
    },
    render: function(){
        return <iframe ref="iframe" {...this.props}/>;
    }
});

var Display = React.createClass({
    getInitialState : function(){
        return {
            oasData : ""
        };
    },
    iframeOnLoad : function(){
        var iframe = this.refs.bannerIframe;
        iframe.contentDocument.open();
        iframe.contentDocument.write(['<head></head><style>body {margin: 0; overflow: hidden;display:inline-block;} html{ margin: 0 auto; text-align: center;} body > a > img {max-width: 100%; height: inherit;}', extraCss, '</style></head><body>', this.state.oasData.Ad[0].Text, '</body>'].join(''));
        iframe.contentDocument.close();
    },
    setOasData : function(data){
        this.setState({
            oasData : JSON.parse(data)
        });
    },
    componentDidMount : function(){
        var url = "getJsonDataUrl";

        var xhttp = new XMLHttpRequest();
        var changeOasDataFunction = this.setOasData;
        xhttp.onreadystatechange = function () {
            if (xhttp.readyState == 4 && xhttp.status == 200) {
                changeOasDataFunction(xhttp.responseText);
            }
        };
        xhttp.open("GET", url, true);
        xhttp.send();
    },
    render : function(){
        return (
            <MyIframe refs="bannerIframe" onLoad={this.iframeOnLoad} />
        );
    }
});

module.exports = Display;

What am I doing wrong?

Null
  • 1,950
  • 9
  • 30
  • 33
user3807940
  • 841
  • 2
  • 8
  • 16
  • Do you intend to load a document into the IFrame element or do you want to set/compose the IFrame content yourself? – Lukas Bünger Jan 12 '16 at 12:15
  • Hi Lukas, I have to set the content myself. In my situation, i make a ajax call to a server to get back somedata. This "somedata" has a scripts which i need to set into the iframe. – user3807940 Jan 12 '16 at 12:57

6 Answers6

129

TLDR;

Edit react-iframe-examples

If you're looking for a way to control the contents of an <iframe> via React in a de-facto canonical way, Portals are the way to go. And as with all things Portal: Once you establish a reference to an existing and mounted DOM node (in this case that would be the contentWindow of a given <iframe>) and create a Portal with it, its contents are also considered children of the «parent» virtual DOM, which means a shared (synthetic) event system, contexts and so on.

Please note that, for code brevity, the examples below make use of the Optional chaining operator, which as of this writing is not supported in all browsers.

Example: A functional React component including hooks:

// iframe.js

import React, { useState } from 'react'
import { createPortal } from 'react-dom'

export const IFrame = ({
  children,
  ...props
}) => {
  const [contentRef, setContentRef] = useState(null)
  const mountNode =
    contentRef?.contentWindow?.document?.body

  return (
    <iframe {...props} ref={setContentRef}>
      {mountNode && createPortal(children, mountNode)}
    </iframe>
  )
}

Example: A React class component:

// iframe.js

import React, { Component } from 'react'
import { createPortal } from 'react-dom'

export class IFrame extends Component {
  constructor(props) {
    super(props)
    this.state = {
      mountNode: null
    }
    this.setContentRef = (contentRef) => {
      this.setState({
        mountNode: contentRef?.contentWindow?.document?.body
      })
    }
  }

  render() {
    const { children, ...props } = this.props
    const { mountNode } = this.state
    return (
      <iframe
        {...props}
        ref={this.setContentRef}
      >
        {mountNode && createPortal(children, mountNode)}
      </iframe>
    )
  }
}

Usage:

import { IFrame } from './iframe'

const MyComp = () => (
    <IFrame>
        <h1>Hello Content!</h1>
    </IFrame>
)

Further control, for example over an <iframe>s <head> contents, can easily be achieved as this Gist shows.

There is also react-frame-component, a package that imho offers pretty much everything you need when working with controlled <iframe>s in React.

Caveats:

  • This answer only addresses use cases, where the owner of a given <iframe> wants to programmatically control (as in deciding about) its contents in a React-ish way.
  • This answer assumes, that the owner of an <iframe> complies with the Same-origin policy.
  • This answer is not suited to track how and when external resources are loaded in an <iframe src="https://www.openpgp.org/> kind of scenario.
  • If accessibility is something you care about, you should give your iframes meaningful title attributes.

Use cases (that I know of);

  • The OP's use case: Ads and the need to control how and when those can access a safely scoped element on your website.
  • Embeddable third-party widgets.
  • My use case (and hence my somewhat informed stance on the matter): CMS UI's, where you want to enable users to preview scoped CSS styles, including applied media queries.

Adding a given set of CSS styles (or stylesheets) to a controlled <iframe>:

As one comment author pointed out, managing styles between a parent application and the contents of a controlled <iframe> can be quite tricky. If you're lucky enough to have (a) dedicated CSS file(s) incorporating all necessary visual instructions for your <iframe>, it might suffice to just pass your IFrame component a <link> tag referencing said style(s), even though this is not the most standard compliant way to go about <link>refs:

const MyComp = () => (
  <Frame>
    <link rel="stylesheet" href="my-bundle.css">
    <h1>Hello Content!</h1>
  </Frame>
) 

In this day and age, however, and especially in a React world, most of the time, build setups create styles and stylesheets on the fly: Because they leverage meta-languages like SASS or even more involved solutions like CSS-in-JS stuff (styled-components, emotion).

This sandbox contains examples of how to integrate some of the more popular styling strategies with iframes in React.

Edit react-iframe-examples

This answer used to also give recipes with regards to versions of React prior to 16.3. At this point in time, however, I think it's safe to say that most of us are able to pull off a React version including Portals, and, to a lesser extent, hooks. If you're in need of solutions with regards to iframes and React versions < 16, hit me up, I'll gladly offer advice.

Sanket Shah
  • 2,888
  • 1
  • 11
  • 22
Lukas Bünger
  • 4,257
  • 2
  • 30
  • 26
  • I tried using using solution and it works. Thanks :). I have another question, since i am using ReactDOM.render("
    blah blah
    ", element), i am unable to render complete html. For example, my response from server has scripts which i need to directly place inside iframe's "" tag. Since we are wrapping it with"
    " the script is not executing. Can you please help?
    – user3807940 Jan 12 '16 at 15:09
  • You actually can target the body directly (not sure about head though), but React will give you a warning. Just assign `el` directly to `frameBody`. If you're handling ads and if they contain stuff like document.write, tbh I don't know whether that works with this approach. – Lukas Bünger Jan 12 '16 at 17:45
  • I go this working by accessing the contentDocument and doing frame.open(), fram.write(["adasdad"]) and frame.close(). – user3807940 Jan 13 '16 at 10:01
  • react-frame-component unfortunately doesn't let you load remote content with the `src` attribute – tom Nov 26 '19 at 05:59
  • 1
    The solution with portals and hooks is incredibly powerful and saved me a lot of tedious hours of work. Thanks for keeping this answer up to date! – Blackus Mar 31 '20 at 09:36
  • I have added my jsx code under the Frame component as shown. Everything works fine, but one thing - the CSS Modules imported is not getting affected on the jsx anymore. If you inspect you can see the pseudo class names attached, but styles are not getting applied. How can I make the "ported" node to get the styles? – know_a_guy_hu_knows_anothr_guy May 04 '20 at 16:42
  • The most trivial solution would be to somehow manually copy the style DOM nodes from the parent to the frame document. You could do that as soon as the mount node exists, maybe use an effect for this make it only happen once. Some CSS-in-JS libs use context providers to pass down styles (emotion comes to mind). It really depends on your setup. – Lukas Bünger May 06 '20 at 12:33
  • 1
    Thank you, this is best resource I've found – piotr_cz May 18 '20 at 17:18
  • I tried to use your example to made a sample : https://codesandbox.io/s/material-demo-forked-gr7hw?file=/demo.js but style from Material-ui is not applied. – Sephyre Aug 20 '20 at 10:53
  • 1
    @Sephyre: The example has nothing to do with material-ui itself, but serves merely to illustrate how one could copy styles from the parent to the nested window. It's really not copy-pastable I'm afraid. material-ui relies on JSS and comes with its own set of rules with regard to how and where styles get injected. This part of their docs should give you a couple of pointers how to approach the problem: https://material-ui.com/styles/advanced/#insertionpoint – Lukas Bünger Aug 20 '20 at 18:26
  • @LukasBünger what is the `styleSelector` supposed to do here? – Richard Zilahi Aug 28 '20 at 10:17
  • I tried, and `React.Children.only()` doesn't seem to be necessary any more, I just pass `children`. Great answer! – w00t Sep 01 '20 at 10:44
  • 1
    @RichardZilahi That was a copy-paste left-over, sorry for that one. I replaced the example with a codesandbox, hoping that this code would make a little more sense than my previous half-assed attempt. – Lukas Bünger Sep 01 '20 at 21:24
  • @w00t Good catch, thanks! I updated the code examples. – Lukas Bünger Sep 01 '20 at 21:25
  • AFAICS you cannot import a font by using `@openfonts` or `typeface`, it simply seems not to work, while online embedding works. Is this true? Or it can be done with these libraries? – GWorking Oct 07 '20 at 19:34
  • @GWorking I think I don't understand what you actually mean or how this is related to the original question. Neither `@openfonts` or `typeface` are CSS attributes/directives that I know about and I'm having a hard time finding online references that would explain either of the two. Would you mind getting a little more specific? – Lukas Bünger Oct 14 '20 at 19:52
  • @LukasBünger These are libraries to import fonts with JavaScript, aimed to facilitate the workflow. Here there is the library link https://github.com/KyleAMathews/typefaces BUT I see now that it is deprecated (!) (@openfonts is equivalent). The beauty of these libraries is that to use a font it is as easy as including `import '@openfonts/kulim-park_latin'` (for example) in your `js` component, and that's it. From my comment I guess this is not working if using them when using your approach (right now I don't remember the details of my comment) – GWorking Oct 15 '20 at 07:36
  • 1
    @GWorking Thanks for the clarification. My guess here is that those libraries use some kind of injection mechanism to add the styles and the fonts to the DOM. In an iframe and with this approach they only ever inject their assets into the top document. If you could somehow identify and select said assets, you could use the "cloned links" approach from the codesandbox examples. – Lukas Bünger Oct 15 '20 at 21:01
  • I think the Hooks-idiomatic way to store refs is to use `React.useRef()`. – Tom Nov 01 '21 at 17:52
  • 1
    @Tom Generally speaking, you're absolutely right. In case of iframes however, we have to wait for the related DOM element to get mounted in order to even render its children, which is not the case for any other DOM element. So the DOM element coming online is in fact a change of state we want to react upon. Hence `useState`. – Lukas Bünger Nov 01 '21 at 19:00
  • How to `postMessage` to this IFrame ?? – Iman Marashi Dec 06 '21 at 11:42
  • 1
    @ImanMarashi `postMessage` is a `Window` method and we already access the instance related to our iframe when deriving the `mountNode` (see 1st example). Basically `contentRef?.contentWindow?.postMessage()` – Lukas Bünger Dec 06 '21 at 14:44
  • How to `addEventListener` to this IFrame? – Iman Marashi Dec 11 '21 at 06:04
  • DOMException: Blocked a frame with origin "http://localhost:19006" from accessing a cross-origin frame. – Iman Marashi Dec 11 '21 at 06:59
  • iframe + Portal = mind blown! – Christophe Feb 28 '22 at 04:24
  • Amazing friend!! could you please also update it using MUI v5 components(which no longer uses the legacy jss )? – Eliav Louski Mar 30 '22 at 08:38
  • @EliavLouski Thanks for the heads up, I'll update the sandbox as soon as I find time. In the meantime and as MUI v5 uses emotion under the hood, you could have a look at the emotion example. – Lukas Bünger Mar 30 '22 at 12:28
15

There is an easier solution if someone just wants to display small HTML inside the iframe.

<iframe src={"data:text/html,"+encodeURIComponent(content)}/>

The max length of content is 32768 characters.

There is also easy to use react-frame-component package which was mentioned in the accepted answer.

Jakub Zawiślak
  • 5,122
  • 2
  • 16
  • 21
11

You can use srcdoc attribute of iframe. It will work!

srcdoc: Inline HTML to embed, overriding the src attribute.

Read: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe

Kaleem Elahi
  • 316
  • 2
  • 14
8

This works too (not supported in IE).

const myHTML = <h1>Hello World</h1>
<iframe srcDoc={myHTML} />

More info here: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe

Sam Walpole
  • 1,116
  • 11
  • 26
3

We can simply use the ReactDOMServer.renderToString() to convert a react component to string and then set it to srcDoc attribute of iframe.

import ReactDOMServer from "react-dom/server";

const MyComponent = () => {
  return <h1>Hello Content!</h1>;
};

const App = () => {
  const srcDoc = <MyComponent />;
  return (
      <iframe
        title="my-iframe"
        width="500px"
        height="500px"
        srcDoc={ReactDOMServer.renderToString(srcDoc)}
      ></iframe>
  );
};

Demo: codesandbox

Tharindu Sathischandra
  • 1,654
  • 1
  • 15
  • 37
  • 1
    The js event handlers like onClick don't work with this solution. Another things are needed to js works inside the iframe. References: https://stackoverflow.com/questions/36323336/onclick-handler-not-registering-with-reactdomserver-rendertostring https://stackoverflow.com/questions/36233309/react-js-serverside-rendering-and-event-handlers – Juanma Menendez Sep 20 '22 at 17:09
1

Using the DOMParser constructor's parseFromString to parse the html is a little simpler than the accepted answer. Here is an example where the parsed html is retrieved from the DOMParser's generated document. If you're sending an element to the iframe, leave out the .body.innerText part of the parseHtml.

class SimpleIframe extends Component {
    render() {
        const parseHtml = html => new DOMParser().parseFromString(html, 'text/html').body.innerText;
        return <iframe srcDoc={parseHtml(this.props.wholeHTMLDocumentString)} />;
    }
}
NathanQ
  • 1,024
  • 18
  • 20