Search code examples
javascriptreactjsdangerouslysetinnerhtml

Javascript dynamically inserted later on: how to make it run?


I have scripts In my React app that are inserted dynamically later on. The scripts don't load.

In my database there is a field called content, which contains data that includes html and javascript. There are many records and each record can include multiple scripts in the content field. So it's not really an option to statically specify each of the script-urls in my React app. The field for a record could for example look like:

<p>Some text and html</p>
<div id="xxx_hype_container">
    <script type="text/javascript" charset="utf-8" src="https://example.com/uploads/hype_generated_script.js?499892"></script>
</div>
<div style="display: none;" aria-hidden="true"> 
<div>Some text.</div> 
Etc…

I call on this field in my React app using dangerouslySetInnerHTML:

render() {
    return (
        <div data-page="clarifies">
            <div className="container">
                <div dangerouslySetInnerHTML={{ __html: post.content }} />
                ... some other data
            </div>
        </div>
    );
}

It correctly loads the data from the database and displays the html from that data. However, the Javascript does not get executed. I think the script doesn't work because it is dynamically inserted later on. How can I make these scripts work/run?

This post suggest a solution for dynamically inserted scripts, but I don't think I can apply this solution because in my case the script/code is inserted from a database (so how to then use nodeScriptReplace on the code...?). Any suggestions how I might make my scripts work?


Update in response to @lissettdm their answer:

constructor(props) {
    this.ref = React.createRef();
}

componentDidUpdate(prevProps, prevState) {
    if (prevProps.postData !== this.props.postData) {
        this.setState({
            loading: false,
            post: this.props.postData.data,
            //etc
        });
        setTimeout(() => parseElements());

        console.log(this.props.postData.data.content);
        // returns html string like: `<div id="hype_container" style="margin: auto; etc.`
        const node = document.createRange().createContextualFragment(this.props.postData.data.content);
        console.log(JSON.stringify(this.ref));
        // returns {"current":null}
        console.log(node);
        // returns [object DocumentFragment]
        this.ref.current.appendChild(node);
        // produces error "Cannot read properties of null"
    }
}

render() {
    const { history } = this.props;
    /etc.
    return (
        {loading ? (
            some code
        ) : (
            <div data-page="clarifies">
                <div className="container">
                    <div ref={this.ref}></div>
                    ... some other data
                </div>
            </div>
        );
    );
}

The this.ref.current.appendChild(node); line produces the error:

TypeError: Cannot read properties of null (reading 'appendChild')


Solution

  • If your are sure about HTML string content is safety and contains a string with valid HTML you can use Range.createContextualFragment() (executes scripts 🚨)

    function App() {
      const ref = useRef();
    
      useEffect(() => {
        /* convert your HTML string into DocumentFragment*/
        const node = document.createRange().createContextualFragment(HTML);
        ref.current.appendChild(node);
      }, []);
    
      return (
        <div>
          <h1>HTML String</h1>
          <div>
            <div ref={ref}></div>
          </div>
        </div>
      );
    }
    
    

    See how script content is executed on JavaScript console working example

    If your are using class component create ref within class constructor, then update node content, I did it in componentDidMount just for testing:

    class App extends React.Component {
      constructor(props) {
        super(props);
        this.ref = React.createRef();
      }
    
      componentDidMount() {
        const node = document.createRange().createContextualFragment(HTML);
        this.ref.current.appendChild(node);
      }
    
      render() {
        return (
          <div>
            <h1>HTML String</h1>
            <div>
              <div ref={this.ref}></div>
            </div>
          </div>
        );
      }
    }
    
    

    see this working example