How to implement adding custom js code snippet feature in React SPA?

Viewed 828

I am building a white label web application. I need to implement adding custom js code snippet feature ("tags" in marketing terms) to let marketers add tracking code like google analytics code and heap code. In the admin app, admins can add custom js code snippet like this.

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-160375581-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'UA-160375581-1');
</script>

As you can see, I need to support both <script src/> and <script>code</script>.

In the web app (Create-React-App SPA), I need to add this code snippet in the document. I have no idea how to implement this feature. I tried to inject this code snippet using React dangersoulySetInnerHTML feature. But innerHTML doesn't allow <script>.

Please look carefully at the code snippet sample, it also has to support external script specified with src attribute.

This screenshot will help you understand what feature I want to implement.

screenshot of instapage builder

3 Answers

I implemented the adding custom script feature like this. Please review the code. I hope you suggest another good solution. The reason I make the cloneScript function is to make HTML script tag that doesn't have "already started" status. If you don't make sense, please check this doc. https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model

const cloneScript = (script) => {
  const s = document.createElement('script');

  // copy attributes
  const attrs = script.attributes;
  for (let i = 0; i < attrs.length; i++) {
    s.setAttribute(attrs[i].name, attrs[i].value);
  }

  // copy inner text of script
  s.text = script.text;

  return s;
};

const addCustomScripts = (custom_tag) => {
  try {
    const parser = new DOMParser();
    const doc = parser.parseFromString(custom_tag, 'text/html');
    const collection = doc.head.children;
    Array.from(collection)
      .map((script) => cloneScript(script))
      .forEach((script) => document.body.appendChild(script));
  } catch (e) {
    console.error(e);
  }
};

const str = `
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-160375581-1"></script>
<script id="stella" data-state="inactive">
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'UA-160375581-1');
</script>`

addCustomScripts(str);

It depends on what kind of tracking you're gonna use.

  1. you can directly edit the html in public/index.html folder

  2. Google Tag Manager has a react package you can use

  3. Or you could transform the script into js yourself

    for example

    const hm = document.createElement("script");
    hm.src = "https://example.com/example.js";
    const s = document.getElementsByTagName("script")[0];
    s.parentNode.insertBefore(hm, s);
    

    look into the source code of react-gtm for a more detailed and polished approach.

  4. Or you could use React Helmet if you don't want to deal with all this, just copy paste and be done with it.

If you cannot render them on the server side, you can do them on the client side but it could be dangerous if the script has malicious content:

Here is a code sample of how you can do it: https://codesandbox.io/s/2rwqy2m0r

import React, { Component, useState, isValidElement }  from "react";
import ReactDOM from "react-dom";

const HtmlComponent = ({ html }) => (
  isValidElement(html) ? html : <div dangerouslySetInnerHTML={{ __html: html }} />
)

class ScriptComponent extends Component {
  componentDidMount() {
    eval(this.props.children);
  }

  render() {
    return null;
  }
}

const htmlTexts = {
  'With h1': {
    description: 'Normal visual HTML elements are rendered corretly',
    html: '<h1>Test</h1>',
  },
  'With script': {
    description: '<script /> tags in HTML are never executed on client side',
    html: '<script>alert(\'test\');</script>',
  },
  'With style': {
    description: '<style /> gets interputed by DOM, and updates element styles',
    html: '<style>body {background: red;}</style>',
  },
  'With script image': {
    description: 'element event actions are triggered so XSS is still possible, even without <scripts /> being evaluated',
    html: '<img src="foo" onerror="(() => alert(\'foo\'))()" />',
  },
  'With button': {
    description: 'element event actions are triggered so XSS is still possible, even without <scripts /> being evaluated',
    html: '<button onclick="(() => alert(\'foo\'))()">Click me</button>',
  },
  'As script element': {
    description: 'Event reacts own script tag doesn\'t evaluate its content',
    html: <script>alert('foo')</script>,
  },
  'As script component': {
    description: 'To run scripts on tag load, a component has to activly execute it',
    html: <ScriptComponent>alert('foo')</ScriptComponent>,
  }
}

function App() {
  const [{ html, description }, setHtml] = useState({});
  return (
    <div className="App">
      <h1>dangerouslySetInnerHTML with script</h1>
      {Object.keys(htmlTexts).map((name, i) => (
        <button
          style={{
            outline: 'none',
            border: 'solid 1px #ddd',
            margin: '10px 10px 30px 0',
            borderRadius: '8px',
            padding: '10px',
            boxShadow: html === htmlTexts[name].html ? '0 0 15px rgba(0,0,0,.3)' : 'none'
          }}
          onClick={() => setHtml(htmlTexts[name])}
        >
          {name}
        </button>
      ))}
      <div>
        {description}
      </div>
      <pre
        style={{
          background: '#222',
          color: '#fff',
          padding: '10px',
        }}
      >
        {isValidElement(html) ? (
          'React Component\n\n' +
          `type: ${typeof html.type === 'string' ? html.type : html.type.name}\n\n` +
          `props: ${JSON.stringify(html.props, null, '  ')}`
        ) : (
          `html:\n\n${html}`
        )}
      </pre>
      <HtmlComponent html={html} />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Related