Loading an external JS file using Gatsby

By

Learn how to load an external JavaScript file in a Gatsby site, like a Wistia embed, by appending the script in gatsby-browser.js from the onClientEntry hook.

~~~

This is a historical Gatsby workaround. For a new project, use the framework integration or the script-loading method recommended by the service you are embedding.

To load an external JavaScript file in a Gatsby site, append a script element to the page from the onClientEntry hook in the gatsby-browser.js file. Let me show you why that’s needed, and how I did it.

In the modern JavaScript Web Development workflow it’s quite common to install JavaScript via npm packages.

Sometimes however we must include an external JavaScript file, and modern tools might make this a little difficult.

In particular I had the need to include a video from Wistia in my site, and after a quick look everything looked quite more complicated than I wanted.

Wistia gave me this HTML snippet to embed:

<script src="https://fast.wistia.com/embed/medias/VIDEOURL.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_VIDEOURL seo=false videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img src="https://fast.wistia.com/embed/medias/VIDEOURL/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

On a “normal” HTML site, maybe built with Hugo like I usually do, it would be dead simple.

I’d just add this code to my page.

Why is this hard in Gatsby?

But in a Gatsby page, which is a React component?

Two problems. First, the snippet mixes markup and script tags, and you can’t just paste HTML with scripts into JSX and expect the scripts to run.

Second, Gatsby pre-renders every page with Node.js at build time. At that point there’s no window and no document, so any code that touches the DOM at the top level of a component breaks the build.

I looked at some plugins but no one really did what I wanted.

The solution is perhaps a bit “hacky”, but worked like a charm and I still feel I have control over what’s happening.

The markup part

I added the HTML code as JSX, properly converting all the HTML attributes: class -> className, aria-hidden -> ariaHidden, and the styles - use a tool like https://magic.reactjs.net/htmltojsx.htm to make it quick.

That covers the div structure. The two script tags are the part we handle separately.

The scripts part

Then I added this code to the gatsby-browser.js file to add the scripts I needed, on page load:

const addScript = url => {
  const script = document.createElement("script")
  script.src = url
  script.async = true
  document.body.appendChild(script)
}

export const onClientEntry = () => {
  window.onload = () => {
    addScript("https://fast.wistia.com/embed/medias/9rvl8vgrzg.jsonp")
    addScript("https://fast.wistia.com/assets/external/E-v1.js")
  }
}

gatsby-browser.js is the right place because Gatsby only runs it in the browser, never during the build. So window and document are guaranteed to exist there.

onClientEntry is a Gatsby browser API that runs once, when the site first loads in the browser. Gatsby sites work as a single page app after the first load, so navigating to other pages does not run it again. The scripts load once and stay available everywhere.

A couple of things to keep in mind. This loads the scripts on every page of the site, not just the one with the video. For a small embed script that’s fine.

Also, assigning window.onload replaces any handler already assigned to it. If something else on your site uses window.onload, use window.addEventListener('load', ...) instead, which can register any number of handlers without conflicts.

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about js: