Executing Dangerously Injected Scripts Inside React Components

By admin
Executing Dangerously Injected Scripts Inside React Components In rare moments, you might want to execute script tags injected with

React
ORG

‘s

dangerouslySetInnerHTML
ORG

prop. Here’s how to make it possible.


First
ORDINAL

off, I hereby declare myself not responsible for you shooting yourself in the foot after reading this.

I’m doing something a little weird with

JamComments
ORG

integrations. When a site’s page is built, all of the HTML,

CSS
ORG

and

JavaScript
PRODUCT

needed for comments to function are pulled in via

REST API
PRODUCT

. I like this pattern. It makes for fewer dependencies, quicker iteration on the product, and it just feels tidier.

For the React-based integrations (Next, Gatsby, or Remix), I’m using the

dangerouslySetInnerHTML
ORG

prop to inject all of that code content into a <JamComments /> component. Using that prop is necessary because I want the code to run as code when the page is rendered.

The injected HTML + CSS execute just fine this way, but not the <script> tags needed for the experience to come alive.

React
PERSON

‘s

dangerouslySetInnerHTML
ORG

prop relies on

innerHTML
GPE

, which deliberately doesn’t execute scripts for [legitimate] security reasons. It’s even in bold,

Christmas
DATE

text within the HTML spec:

I respect that. Security matters. But in my case, executing

JamComments
ORG

‘ scripts "dangerously" was just an implementation hurdle by nature of integrating with React. Their own documentation even makes the caveat that using it might be appropriate, as long as the injected code is coming from an "extremely trusted source." I know exactly what’s being injected here, so I feel like I deserve a hall pass.

Fortunately, there’s a straightforward way to make this happen if you ever need it. The TL;DR: after the component mounts, scoop up the stringified code and re-run it in a document fragment.

Let’s build that out. Here’s the shell of what we’ll start with:

import { useRef , useLayoutEffect } from ‘react’ ; export function

DangrousElement
ORG

( { markup } ) { const elRef = useRef < HTMLDivElement > ( ) ;

useLayoutEffect
CARDINAL

( ( ) => { } , [ ] ) ; return ( < div ref = { elRef } dangerouslySetInnerHTML = { { __html : markup } } </ div > ) ; }

Reaching for

useLayoutEffect
CARDINAL

() is very intentional, by the way. More on that later. For now, let’s continue fleshing this out with the following snippet code-as-a-string being passed to the component:

const markup = ` <span>Dangerous markup!</span> <script>console.log("Script has executed!");</script> ` ; < DangrousElement markup = { markup } />

If we do this right, we’ll see that console.log fire after our component mounts in our application.

If we wanted, we could query that <div> for the <script> tags ourselves, build clones of them, and then execute them by appending them to an existing

DOM
ORG

node. But there’s a cleaner way to pull this off: a document fragment, or a little version of an HTML document we can use to contain (and execute) code.

We’re going to reach for a specific piece of that

API
ORG

.

First
ORDINAL

, we’ll create a document "range" to hold our fragment. Think of this as a slice of the document we’ll use to "hold" everything. After that, we’ll set the context of that range to the specific

DOM
ORG

node we’re building the component around, and finally, create the fragment. Here’s what we’ll put into that useLayoutEffect() hook:

const range = document . createRange ( ) ; range . selectNode ( elRef . current ) ; const documentFragment = range . createContextualFragment ( markup ) ;

The benefit to this approach is that the code will execute in the context of that node. We can verify this by creating a couple of empty ranges.

First
ORDINAL

, we’ll make one with no "selected" node.

const openRange = document . createRange ( ) ;

When we access the children of the startContainer property, it’s wrapped around the entire document itself:

But that changes if we target a specific node:

const contextualRange = document . createRange ( ) ; contextualRange . selectNode ( document . getElementById ( "app" ) ) ;

This time, the range is wrapped around the node we selected, giving it a little more specific context:

Depending on the script you’re running, this might not make for anything practically meaningful, but it’s nice to know that the code will run as if it were always in that spot, and maybe prevent some unexpected context-related bugs at the same time.

The last part’s simple. Wipe the already-rendered code and replace it with our fragment:

import { useRef, useLayoutEffect } from ‘react’; export function

DangrousElement
ORG

({ markup }) { const elRef = useRef<HTMLDivElement>();

useLayoutEffect
CARDINAL

(() => { const range = document.createRange(); range.selectNode(elRef.current); const documentFragment = range.createContextualFragment(markup); + // Inject the markup, triggering a re-run! + elRef.current.innerHTML = ”; + elRef.current.append(documentFragment); }, []); return ( <div ref={elRef}

dangerouslySetInnerHTML=
GPE

{{ __html: markup }} </div> ); }

Now, we can see that script execute like desired:

It’s uncommon to see, but choosing

useLayoutEffect
CARDINAL

() here is important. This hook fires before the component has a chance to be rendered (and painted) to the screen. This differs from

useEffect(
ORG

) , which runs after render. If we want our code to execute as if it were there from the beginning, our work needs to come before any possible paint, which could cause an unwelcome "flicker" for scripts that mess with the

DOM
ORG

. To satisfy your curiosity, here’s a modified version of the code string:

const markup = ` <span

id="message">First!</span
PERSON

> <script> synchronousWait(1500); document.getElementById("message").innerText = "

Second
ORDINAL

!"; </script> ` ;

I’ve made a synchronousWait() method that blocks the main thread for

1.5 seconds
TIME

. After that, the text is updated in the injected <span> . Because React gives the component a chance to do an initial render + paint with our code, mounting it allows us to see the initial text before it’s swapped out.

Replacing that with

useLayoutEffect
CARDINAL

() guarantees execution before paint, preventing this flash. Instead, you just get a blank, 1.5s delay – just like you’d get if the snippet were to run in a fresh HTML document.

The trade-off should be obvious, though: because the hook executes synchronously, you’re at risk for introducing some annoying performance issues. So, be wary about what the injected script is doing, and be sure to lean into asynchronous patterns as needed.

You’ll notice our log fired once, which is what’d we’d expect in a production environment. But with

React
PERSON

‘s strict mode enabled,

useLayoutEffect
CARDINAL

() will fire twice in development mode, meaning our log would also fire twice unless extra steps are made to avoid it. I’ve intentionally disabled strict mode here, just for the sake of conceptual simplicity.

If you’d like to see a version that works regardless of strict mode, you can see the

StackBlitz
ORG

environment here. It’s not much – just another useRef() hook and some value setting/getting.