tag into your page." }
tutorial // Nov 19, 2021

How to Load Third-Party Scripts Dynamically in JavaScript

How to dynamically load a JavaScript library like Google Maps by writing a script to automatically inject a tag into your page.

How to Load Third-Party Scripts Dynamically in JavaScript

Getting Started

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start:

Terminal

cd app && joystick start

After this, your app should be running and we're ready to get started.

Creating a dynamic script loader

In JavaScript, a common practice is loading in other packages and libraries into your app. Traditionally this is done via a package manager like NPM (Node Package Manager), but sometimes, we need to load JavaScript dynamically.

"Dynamically" can also be read as "on the fly" or "from a third-party server." Generally the reason why we do this is that the script in question requires an API key or some other form of authentication before the script can load (or, the script is hosted remotely for security purposes to avoid it being tampered with).

While we can add a <script></script> tag directly into our main index.html file, this is likely overkill as you will only need certain scripts on certain pages. To get around this, we can write a dynamic script loader that can be called on-demand from pages where a script is necessary.

/lib/loadScript.js

const urls = {
  googleMaps: `https://maps.googleapis.com/maps/api/js?key=${joystick.settings.public.googleMaps.apiKey}&libraries=places`,
};

export default (name = '', callback = null) => {
  const url = name && urls[name];

  if (!name || !url) {
    throw new Error(`Must pass the name of a supported script: ${Object.keys(urls).join(', ')}`);
  }
};

Getting started with our script, our goal is to create a function that we can import wherever we need it in our code. To make that possible, here, we create a file where we export default a function taking two arguments:

  1. name - The name of the script that we're trying to load.
  2. callback - A callback function to call after our script has loaded.

For name, we expect this to be a name we've created. In our example here, we're going to load the Google Maps JavaScript API. Up top, we can see an object being created urls which has a property googleMaps defined on it, set to the URL Google gives us for their JavaScript library.

Note: if you don't have an API key for Google Maps, you can obtain one following the steps here.

In the URL here, we've replaced the apiKey query parameter that Google Maps expects with a pointer to a global value from the settings file in our app: joystick.settings.public.googleMaps.apiKey.

Here, joystick.settings is a global value in the browser automatically populated with the contents of the settings file located in /settings.development.json at the root of our app. Making use of this convention here, we're saying that we expect there to be a value in that settings file located at apiKey nested in the public.googleMaps object, like this:

/settings.development.json

{
  "config": {
    "databases": [
      {
        "provider": "mongodb",
        "users": true,
        "options": {}
      }
    ],
    "i18n": {
      "defaultLanguage": "en-US"
    },
    "middleware": {},
    "email": {
      "from": "",
      "smtp": {
        "host": "",
        "port": 587,
        "username": "",
        "password": ""
      }
    }
  },
  "global": {},
  "public": {
    "googleMaps": {
      "apiKey": "apiKey1234"
    }
  },
  "private": {}
}

So it's clear, the line https://maps.googleapis.com/maps/api/js?key=${joystick.settings.public.googleMaps.apiKey}&libraries=places above will be read by JavaScript as https://maps.googleapis.com/maps/api/js?key=apiKey1234&libraries=places. The punchline being that the variable passed in the ${} part will be replaced by the value in our settings file (this is known as JavaScript string interpolation).

/lib/loadScript.js

const urls = {
  googleMaps: `https://maps.googleapis.com/maps/api/js?key=${joystick.settings.public.googleMaps.apiKey}&libraries=places`,
};

export default (name = '', callback = null) => {
  const url = name && urls[name];

  if (!name || !url) {
    throw new Error(`Must pass the name of a supported script: ${Object.keys(urls).join(', ')}`);
  }
};

Focusing back on our code, with our API key embedded, assuming that our urls object has a property matching the name argument passed to our loadScript() function, just inside of that function we attempt to get the URL for the script we want to load with name && urls[name]. This says "if name is defined and you can find a property on the urls object matching this name, return its value to us."

In JavaScript, this urls[name] is known as "bracket notation." This allows us to dynamically retrieve values from an object using some variable or value. To be clear, if our urls object had a property pizza set to https://marcospizza.com defined on it and we passed 'pizza' as the name for our script, we'd expect the url variable here to be set to https://marcospizza.com.

Just below this, to be safe, we do a quick check to say "if we don't have a name defined, or, we don't have a url defined` throw an error." This will prevent our script from loading and warn us in the browser console so we can fix the issue.

/lib/loadScript.js

const urls = {
  googleMaps: `https://maps.googleapis.com/maps/api/js?key=${joystick.settings.public.googleMaps.apiKey}&libraries=places`,
};

export default (name = '', callback = null) => {
  const url = name && urls[name];

  if (!name || !url) {
    throw new Error(`Must pass the name of a supported script: ${Object.keys(urls).join(', ')}`);
  }

  const existingScript = document.getElementById(name);

  if (!existingScript) {
    const script = document.createElement('script');
    script.src = url;
    script.id = name;
    document.body.appendChild(script);

    script.onload = () => {
      if (callback) callback();
    };
  }

  if (existingScript && callback) callback();
};

Building out the rest of our function, now we get into the fun stuff. Assuming that a name was passed and matched a property on our urls object (meaning we got back a url), the next thing we need to do is make sure that we haven't already loaded the script in question before.

This is important! Because we're loading JavaScript dynamically, generally speaking, there's potential for our function be called multiple times (either intentionally or accidentally). Because our script is going to append or add a <script></script> tag to our HTML, we want to prevent creating duplicates of it. Here, we look for an existing <script></script> tag with an id attribute equal to the name we passed in to loadScript.

If we find it, we jump down to the bottom of our function, and, assuming we have a callback function defined, call that function (signaling that "yes, this script was already loaded and can be used").

If we don't find an existingScript, we want to load it dynamically. To do it, first, we create a new <script></script> tag element in memory (meaning it's not rendered to the page yet, just in the browser's memory storage). We expect this to create a DOM element (an object as far as our code is concerned) which we store in the variable script.

On that object, we can set attributes on our new <script></script> tag dynamically. Here, we want to set to the src attribute to the url we obtained from the urls object above and the id attribute to the name we passed in to loadScript().

With those attributes set, our script is ready to be appended or "rendered" to our browser's HTML. To do it, we call to document.body.appendChild() passing in our script variable (JavaScript will recognize the format of the object as a valid DOM element and append it as requested). Because we're saying document.body here, we can expect this <script></script> tag to literally be appended as the last element inside of our HTML's <body></body> tag:

x7owdIPFmWFmGUnc/vjG2RibpzIcPOueC.0
The <script></script> tag appended to <body><body>.

Finally, after our script is appended, we assign an onload function to it which is the function our browser will call once the file located at the url we set to src is loaded. Inside, if our callback is defined, we call it.

That does it for our loader's definition. Next, let's take a look at putting it to use and see how this works.

Calling the dynamic script loader

To put our loader to use, we're going to make use of the components feature built-in to the Joystick framework we started with at the beginning of the tutorial. When we ran joystick create app, we were automatically given a component at /ui/pages/index/index.js in our project. Let's open that file up and pull in our loadScript() function.

/ui/pages/index/index.js

import ui, { get } from "@joystick.js/ui";
import Quote from "../../components/quote";
import loadScript from "../../../lib/loadScript";

const Index = ui.component({
  lifecycle: {
    onMount: (component) => {
      loadScript('googleMaps', () => {
        new google.maps.Map(document.getElementById("map"), {
          center: { lat: -34.397, lng: 150.644 },
          zoom: 8,
        });
      });
    },
  },
  methods: { ... },
  events: { ... },
  css: `
    div p {
      font-size: 18px;
      background: #eee;
      padding: 20px;
    }

    #map {
      width: 100%;
      height: 300px;
    }
  `,
  render: ({ component, i18n }) => {
    return `
      <div>
        <p>${i18n("quote")}</p>
        ${component(Quote, {
          quote: "Light up the darkness.",
          attribution: "Bob Marley",
        })}
        <div id="map"></div>
      </div>
    `;
  },
});

export default Index;

Up top, we import loadScript from the /lib/loadScript.js path where we created it (omitting the .js on the end is fine here as our build tool will automatically attempt to load a .js file at this URL as part of the import process).

The part we want to pay attention to is the lifecycle.onMount function being defined near the top of our component. If we look inside that function, we're calling to our loadScript() function first passing the name of the script we want to load, followed by our callback function. Look close at the callback. Remember: our goal is to load the Google Maps library so we can put it to use immediately after it's loaded. Here, because our callback is fired after our script is loaded, we can assume that Google Maps is available.

Following that assumption, we make a call to the new google.maps.Map() method, passing in the DOM node where we want to load our map (if we look down in the render() function of our component, we can see a <div id="map"></div> being rendered as a placeholder where our map should be rendered. Here, we say document.getElementById() to get that <div></div> element's DOM node in the browser.

That's it. If we take a look at our app in the browser at http://localhost:2600 after a few milliseconds we should see our Google Map load (if not, double-check your API key and that any ad blockers are turned off).

Wrapping up

In this tutorial, we learned how to write a function to help us dynamically create and inject a <script></script> tag into our HTML. To do it, we took in the name of a script and mapped it to a URL where that script lives on an object and then used the document.createElement() function from JavaScript to create a script tag before appending it to the <body></body> tag in our DOM. Finally, we learned how to call to our loadScript() function to render a Google Maps map to the page.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode