When we're building single-page or JavaScript driven applications using tools like Pup, from time to time we need to rely on third-party libraries that require a script tag being placed in the <head></head> or <body></body> of our HTML.

For libraries that are used globally across the site, placing these in our main <head></head> is perfectly fine.

For libraries that are only used in one or a few places, placing calls to load these libraries in our application's <head></head> can add unnecessary page load, and subsequently, load time.

Fortunately, there's a workaround.

The trick is to create the <script></script> tag loading the library on the fly and inject it into the DOM only when we need it. Here's the gist of it:

Dynamic Script Loading Template

const loadDynamicScript = (callback) => {
  const existingScript = document.getElementById('scriptId');

  if (!existingScript) {
    const script = document.createElement('script');
    script.src = 'url'; // URL for the third-party library being loaded.
    script.id = 'libraryName'; // e.g., googleMaps or stripe
    document.body.appendChild(script);

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

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

The idea here is that we create a function that we can call on the pages where we need the third-party library and dynamically create and inject the <script></script> tag into the <body></body> of the application.

To make this concrete, let's assume we're trying to load the Google Maps API to display a map on our page:

Loading Google Maps Dynamically

const loadGoogleMaps = (callback) => {
  const existingScript = document.getElementById('googleMaps');

  if (!existingScript) {
    const script = document.createElement('script');
    script.src = 'https://maps.googleapis.com/maps/api/js?key=<API Key>&libraries=places';
    script.id = 'googleMaps';
    document.body.appendChild(script);

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

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

Let's step through it. Inside of our function, we begin by trying to detect whether or not we've already loaded Google Maps by looking for a <script></script> tag with its id set to googleMaps. If we do, we stop—there's no need to load the library twice.

If we don't find an existingScript, however, we go to actually create the script dynamically. We start by creating an empty <script></script> tag in memory as script and then assign the necessary attributes to it src (the URL where the script lives) and the id to identify the script later. Finally, we append the script to our <body></body> tag to actually load it.

One last detail: notice the call to script.onload here? This is to give us control over loading elements in our UI that are dependent on the script existing. The callback argument passed to our loadGoogleMaps function here is designed to call some code after the library has loaded.

Notice that we call the callback() if it exists both when we're loading the script for the first time, as well as on subsequent calls where the script already exists (thanks to Bob Johnson in the comments for pointing this out).

Putting this into practice, here's an example of putting this to use in a React component that exists in Pup:

Example Usage

import React from 'react';
import GoogleMap from '../GoogleMap/GoogleMap';
import loadGoogleMaps from '../../../modules/load-google-maps.js';

class MapsComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { googleMapsReady: false };
  }

  componentWillMount() {
        loadGoogleMaps(() => {
          // Work to do after the library loads.
          this.setState({ googleMapsReady: true });
        });   
  }

  render() {
      return (
          <div className="MapsComponent">
                {this.state.googleMapsReady ? <GoogleMap /> : ''}
      </div>
    );
  }
}

That's it! Now, when our component is beginning to mount, we'll call to our library to dynamically create and inject our <script></script> into the <body></body>. Once it's loaded and calls to the script.onload function, the callback we pass here will fire and update our component's this.state.googleMapsReady value to be true. If we look down in our render function, we can see a contrived example of this at play. Here, we only load our <GoogleMap /> component if this.state.googleMapsReady is true, meaning, our library is loaded and ready for use.

Neat, right? Though we've focused on Google Maps as our example here, this will work for any third-party library that needs to be loaded via a remote URL.