11

This is somewhat similar to this question:

Adding script tag to React/JSX

But in my case I am loading a script like this:

<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','ID');</script>
<!-- End Google Tag Manager -->

Now I know there is a npm package for the google tag manager but I am curious if I would like to do this in a custom way how would I go about?

In the above question I see a lot of:

const script = document.createElement("script");

script.src = "https://use.typekit.net/foobar.js";
script.async = true;

document.body.appendChild(script);

Which is fine but if I have a function inside of the loaded script how would I go about executing this correctly?

CodingLittle
  • 1,761
  • 2
  • 17
  • 44
  • make that function a global variable, then use it anywhere like this: `gloabl.functionFromScript`. Also, why would you do that, there must be a better solution to whatever is your actual use case. – Vaibhav Vishal Feb 11 '21 at 13:05
  • Also if you want to have the custom script always loaded, just add it to your `index.html` instead of going through all that js to attach a script tag to body – Vaibhav Vishal Feb 11 '21 at 13:06
  • You are correct. The script above will be moved to index.html but I have a different scenario where something similar is added BUT it is only used in specific scenarios so in order to not load in global scripts that are basically just sitting there until a user comes to a specific scenario isn't ideal so I thought I would load it once a user triggers the desired behavior. I would like to avoid to save it in a global variable hence the same scenario. I have a HOOK which I want to trigger a script written in a similar way as above but can't figure out how to do it properly. – CodingLittle Feb 11 '21 at 17:19
  • Can you not attach your function to an object and then expose it for other scripts to consume. Yes, the new object will ultimately be attached to global/window but what else can be done. – pixlboy Feb 19 '21 at 15:17

2 Answers2

11

To add a random script like this, you could:

  1. Add the script to your index.html
  2. Paste the code to a file and use an import statement.
  3. Dynamically load the script once the user does something, using code splitting.

1. Adding the script to your HTML

Just stick the script tags in your index.html file, preferably at the end of the body tags. If using create-react-app, the index.html file is located in the public directory:

<body>
  <div id="root"></div>
  <script>/* your script here */</script>
</body>

2. Import from file

Alternatively, you could paste the script into a .js file, and import it from anywhere in your code. A good place to import general scripts would be in your index.js entry point file. This approach has the benefit of including the script with the rest of your js bundle, enabling minification and tree shaking.

// index.js
import "../path/to/your/script-file";

3. Code splitting Lastly, if you would like to dynamically load a piece of js code in a certain point in time, while making sure it isn't part of your starting bundle, you could do code splitting, using dynamic imports. https://create-react-app.dev/docs/code-splitting

function App() {
  function handleLoadScript() {
    import('./your-script')
      .then(({ functionFromModule }) => {
        // Use functionFromModule 
      })
      .catch(err => {
        // Handle failure
      });
  };

  return <button onClick={handleLoadScript}>Load</button>;
}
deckele
  • 4,623
  • 1
  • 19
  • 25
4

Usually, one can update an HTML element in react using the dangerouslySetInnerHTML prop. But for the case of a script that is to be executed, this won't work, as discussed in this other SO question.

An option you have to achieve this, is appending the element inside a new document context, using the document Range API, createContextualFragment

Working example below. Note that I've tweaked your script a bit to show some ways to customize it.

const { useState, useRef, useEffect, memo } = React;

const MyCustomScriptComponent = () => {
  const [includeScript, setIncludeScript] = useState(false)

  // just some examples of customizing the literal script definition
  const labelName = 'dataLayer'
  const gtmId = 'ID' // your GTM id

  // declare the custom <script> literal string
  const scriptToInject = `
    <script>
(function(w,d,s,l,i){
const gtmStart = new Date().getTime();
w[l]=w[l]||[];w[l].push({'gtm.start':
gtmStart,event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
console.log("loaded at gtmStart:", gtmStart);
})(window,document,'script','${labelName}','${gtmId}');
console.log("fetching GTM using id '${gtmId}'");
     </script>`

  const InjectScript = memo(({ script }) => {
    const divRef = useRef(null);

    useEffect(() => {
      if (divRef.current === null) {
        return;
      }
      // create a contextual fragment that will execute the script
      // beware of security concerns!!
      const doc = document
          .createRange()
          .createContextualFragment(script)
      
      // clear the div HTML, and append the doc fragment with the script 
      divRef.current.innerHTML = ''
      divRef.current.appendChild(doc)
    })

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

  const toggleIncludeScript = () => setIncludeScript((include) => !include)

  return (
    <div>
      {includeScript && <InjectScript script={scriptToInject} />}
      <p>Custom script {includeScript ? 'loaded!' : 'not loaded.'}</p>
      <button onClick={toggleIncludeScript}>Click to load</button>
    </div>
  )
}

ReactDOM.render(<MyCustomScriptComponent />, document.getElementById('app'))

Try it live on codepen.

For additional reference, you can find more alternatives to inject a script in this medium post.

tmilar
  • 1,631
  • 15
  • 19