How to Execute Rendered Script Tags with dangerouslySetInnerHTML in React?

If you’ve been using dangerouslySetInnerHTML to render raw HTML strings in React and realised that it doesn’t execute the script tags in the HTML string, then the solutions in this article will help you.

The reason why script tags are not executed via dangerouslySetInnerHTML is because React internally uses innerHTML to inject or add the raw HTML string into the DOM. Browsers do not execute the script tags in the HTML string when set via innerHTML.

So what are our solutions ? The cleanest solution is to use the Range API. But if you are using a third party library like html-react-parser to render raw HTML strings as React elements, then there’s a way to solve for that as well.

Range API

The idea is first drop using dangerouslySetInnerHTML altogether. Then get direct access to the DOM node inside which we want to render the raw HTML string. A good way to achieve this in React is by using Refs. Finally, we have to use the Range API’s createContextualFragment method to convert our htmlString into DOM nodes and add it into the node (stored in the ref). Browsers do execute the script tags when using createContextualFragment (unlike innerHTML).

I’ve already written about this approach in vanilla JavaScript before. To use it in React, all we have to do is combine it with useEffect.

function Component(props) {
  const divRef = useRef();

  const htmlString = `
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.0.0/umd/react.production.min.js"></script>
    <script>alert('Hello React!');</script>
  `;

  useEffect(() => {
    const fragment = document.createRange().createContextualFragment(htmlString);
    divRef.current.append(fragment);
  }, []);

  return <div ref={divRef} />;
}

Since we pass an empty array to useEffect, the effect will only run after the first render (like componentDidMount). In some cases it may be a better idea to pass [htmlString, divRef] instead of []. If for some reason you’d like to run the effect (appending htmlString) after every render then remove the dependancy array.

Although the effect code is pretty small, if you have to put it in multiple places in your app or don’t feel like writing it at all, then there’s a third party package called dangerously-set-html-content that you can use. This library pretty much does what I showed above and exposes a React component for usage.

import InnerHTML from 'dangerously-set-html-content';

function Component(props) {
  const htmlString = `...`;

  // Renders <div>{htmlString}</div>
  return <InnerHTML html={html} />;
}

The InnerHTML component wraps the htmlString inside a div tag and returns/renders.

HTML to React Parsers

Most of the widely used third party packages that allows us to parse and convert HTML string into its React element counterparts will provide an option to execute a callback for every React element that it creates for the DOM tags present in the HTML string. It maybe a part of their “transform” or some kind of pre-processing option.

In that callback, for every script React element, we can create a brand new DOM element node and append it to our HTML document’s head or body. For instance if we were using the html-react-parser library, we could do this:

import parse from 'html-react-parser';

function Component(props) {
  const htmlString = `
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.0.0/umd/react.production.min.js"></script>
    <script>alert('Hello React!');</script>
  `;

  const reactElement = parse(htmlString, {
    replace: (node) => {  
      if (node.type === 'script') {
        let externalScript = node.attribs.src ? true : false;
        
        const script = document.createElement('script');
        if (externalScript) {
          script.src = node.attribs.src;
        } else {
          script.innerHTML = node.children[0].data;
        }
        
        document.head.append(script);
      }
    }
  });

  return reactElement;
}

The replace callback option is called once for every React element created by the library with its own node representation that you can console.log to inspect its data. Whatever we need, i.e., a script tag’s attributes (src for external scripts) and contents (innerHTML for inline scripts) will be available in the node object.

Using the data in this object we can create our own script DOM nodes and append to our HTML document to trigger their execution.

RegExp

There’s an “unclean” approach as well where we can extract the script src (for external scripts) and content (for inline scripts) from the HTML string using a regular expression (str.matchAll(regExp)) and then inside a useEffect():

  • eval(scriptContents) or (new Function(scriptContents))() for inline scripts.
  • Create a script element with document.createElement('script'), set the src and append to document.head or document.body for external scripts.

Leave a Reply

Your email address will not be published.