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 withdocument.createElement('script')
, set thesrc
and append todocument.head
ordocument.body
for external scripts.