Composables

useScript

Load third-party scripts with SSR support and a proxied API.

Stable as of v1.9

Features

  • 🪨 Turn a third-party script into a fully typed API
  • ☕ Delay loading your scripts until you need them: manual or Promise
  • 🚀 Best performance and privacy defaults
  • 🎃 Easily hook into script events: onload, onerror, etc
  • 🪝 Proxy API: Use a scripts functions before it's loaded (or while SSR)
  • 🇹 Fully typed APIs

Background

Loading scripts using the useHead composable is easy.

Google Analytics
useHead({
  script: [
    // Google Analytics Setup
    { innerHTML: `window.dataLayer = window.dataLayer || [], window.gtag = function gtag(...p) { window.dataLayer.push(p) }, window.gtag('js', new Date()), window.gtag('config', options.id);` },
    // Load the script
    { src: 'https://www.googletagmanager.com/gtm.js?id=GTM-MNJD4B' }
  ]
})

However, when loading a third-party script, you often want to access some functionality provided by the script.

For example, Google Analytics provides a gtag function that you can use to track events.

// We need to load first: https://www.google-analytics.com/analytics.js
gtag('event', 'page_view', {
  page_title: 'Home',
  page_location: 'https://example.com',
  page_path: '/',
})

The API provided by these scripts doesn't work in an SSR environment or if the script isn't loaded yet. Leading to a jumbled mess of trying to make sure we can use the API. For TypeScript you'll need to augment global window types to use the API effectively.

The useScript composable aims to solve these issues and more with the goal of making third-party scripts a breeze to use.

const { gtag } = useScript<GoogleTag>('https://www.google-analytics.com/analytics.js', {
  beforeInit() {
    // Google Analytics Setup
    window.dataLayer = window.dataLayer || []
    window.gtag = function gtag(...p) { window.dataLayer.push(p) }
    window.gtag('js', new Date())
    window.gtag('config', options.id)
  },
  use() {
    return { gtag: window.gtag }
  }
})
// fully typed, usable in SSR and when lazy loaded
gtag('event', 'page_view', {
  page_title: 'Home',
  page_location: 'https://example.com',
  page_path: '/',
})

interface GoogleTag {
  gtag: ((fn: 'event', opt: string, opt2: { [key: string]: string }) => void)
}

Usage

Your First Script

The simplest usage of the useScript composable is to load a script and use the API it provides. To do so you need a URL to the script and a function that resolves the API use().

const instance = useScript('https://example.com/my-awesome-script.js', {
  // The `use` function will only called client-side, it's used to resolve the API
  use() {
    return window.myAwesomeScript
  }
})

Done, but accessing the API should provide type safety. To do so, you can use the useScript composable with a generic type.

interface MyAwesomeScript {
  myFunction: ((arg: string) => void)
}
const { myFunction } = useScript<MyAwesomeScript>('https://example.com/my-awesome-script.js', {
  use() {
    return window.myAwesomeScript
  }
})

// fully typed, usable in SSR and when lazy loaded
myFunction('hello')

Because useScript returns a Proxy API, you can call the script functions before it's loaded. This will noop for SSR and be stubbable.

This also lets you load in the script lazily while still being able to use the API immediately.

const { myFunction } = useScript<MyAwesomeScript>('https://example.com/my-awesome-script.js', {
  trigger: 'manual',
  use() {
    return window.myAwesomeScript
  }
})

// only client-side it will be called when the script is finished loaded
myFunction('hello')

Script Deduping

By default, your scripts will be deduped based on the script src.

const instance = useScript('/my-script.js')
const instance2 = useScript('/my-script.js')
// instance2 will return the same reference as instance without loading a new script

In cases where the src is dynamic and you're using it in multiple places, you should provide a key to the script options.

const instance = useScript({ key: 'my-script', src: '/123.js' })
const instance2 = useScript({ key: 'my-script', src: '/456.js' })
// instance2 will return the same reference as instance without loading a new script

Triggering Script Load

The trigger option is used to control when the script is loaded by the browser.

It can be one of the following:

  • undefined | client: Script tag will be inserted as the useScript is hydrated on the client side. The script will be usable once the network request is complete.
  • manual: Load the script manually using the $script.load method. Only runs on the client.
  • Promise: Load the script when the promise resolves. This allows you to load the script after a certain time or event, for example on the requestIdleCallback hook. Only runs on the client.
  • Function: Load the script when the function is called. Only runs on the client.
  • server: Insert the script tag into the SSR HTML response (<script src="...">).

When you're using a trigger that isn't server, the script will not exist within your SSR response, meaning it will only load client-side.

Manual
const { $script } = useScript('https://example.com/script.js', {
  trigger: 'manual'
})
// ...
$script.load()

Waiting for Script Load

Sometimes you'll want to directly use the script instead of relying on the proxy. For this you can use the $script object as a Promise.

const { $script } = useScript<MyAwesomeScript>('https://example.com/my-awesome-script.js', {
  use() {
    return window.myAwesomeScript
  },
})
// Note: Do not do this if you have a `manual` trigger
$script.then((myAwesomeScript) => {
  // accesses the script directly, proxy is not used
  myAwesomeScript.myFunction('hello')
})
// OR - will block rendering until script is available
const myAwesomeScript = await $script
myAwesomeScript.myFunction('hello')

When you have a manual trigger awaiting the promise will never resolve unless you load() the $script.

const { $script } = useScript<MyAwesomeScript>('https://example.com/my-awesome-script.js', {
  use() {
    return window.myAwesomeScript
  },
  trigger: 'manual'
})

// Warning: Will never resolve!
await $script

// Make sure you call load if you're going to await with a manual trigger
$script.load()

Removing a Script

When you're done with a script, you can remove it from the document using the $script.remove() method.

const { $script } = useScript<MyAwesomeScript>('https://example.com/my-awesome-script.js')

$script.remove()

The remove() function will return a boolean indicating if the script was removed.

Handling Script Loading Failure

Sometimes scripts just won't load, this can be due to network issues, the script being blocked, etc.

To handle this, you can catch exceptions thrown from $script.

const { $script } = useScript<MyAwesomeScript>('https://example.com/my-awesome-script.js', {
  use() {
    return window.myAwesomeScript
  },
})

$script.catch((err) => {
  console.error('Failed to load script', err)
})

Otherwise, you always check the status of the script using $script.status.

const { $script } = useScript<MyAwesomeScript>('https://example.com/my-awesome-script.js', {
  use() {
    return window.myAwesomeScript
  },
})
$script.status // 'awaitingLoad' | 'loading' | 'loaded' | 'error'

SSR Stubbing

In cases where you want to use the script API on the server, you can use the stub option. This lets you call your script functions and handle them in a way that makes sense for your server.

For example, we can stub the gtag function to send events on the server to Google Analytics. Meaning you have a single API to use for both server and client to achieve the same result.

const { gtag } = useScript<GoogleTag>('https://www.google-analytics.com/analytics.js', {
  use() {
    return { gtag: window.gtag }
  },
  stub() {
    if (process.server) {
      return (fn: 'event', opt: string, opt2: { [key: string]: string }) => {
        // send fetch to ga
        return fetch('https://www.google-analytics.com/analytics.js', {
          method: 'POST',
          body: JSON.stringify({ event: opt, ...op2 })
        })
      }
    }
  }
})

API

useScript<API>(scriptOptions, options)

Argument: Script Options

The script options, this is the same as the script option for useHead. For example src, async, etc.

A shorthand for the src option is also available where you can just provide the URL as a string.

Simple
useScript('https://www.google-analytics.com/analytics.js')
Object
useScript({
  key: 'google-analytics', // custom key
  src: 'https://www.google-analytics.com/analytics.js',
  async: true,
  defer: true,
})

Argument: Use Script Options

use

  • Type: () => API

A function that resolves the scripts API. This is only called client-side.

const { trackPageview } = useScript<FathomApi>({
  // fathom analytics
  src: 'https://cdn.usefathom.com/script.js',
}, {
  use: () => window.fathom
})
// just works
trackPageview({ url: 'https://example.com' })

trigger

  • Type: 'undefined' | 'manual' | Promise<void>

A strategy to use for when the script should be loaded. Defaults to undefined.

useScript({
  src: 'https://example.com/script.js',
}, {
  trigger: new Promise((resolve) => {
    setTimeout(resolve, 10000) // load after 10 seconds
  })
})

stub

A more advanced function used to stub out the logic of the API. This will be called on the server and client.

This is particularly useful when the API you want to use is a primitive and you need to access it on the server. For instance, pushing to dataLayer when using Google Tag Manager.

const { sendEvent, doSomething } = useScript<MyScriptApi>({
  src: 'https://example.com/script.js',
}, {
  use: () => window.myScript,
  stub: ({ fn }) => {
    // stub out behavior on server
    if (process.server && fn === 'sendEvent')
      return (opt: string) => fetch('https://api.example.com/event', { method: 'POST', body: opt })
  }
})
// on server, will send a fetch to https://api.example.com/event
// on client it falls back to the real API
sendEvent('event')
// on server, will noop
// on client it falls back to the real API
doSomething()

Return Value

The useScript composable returns a Proxy API that you can use to interact with the script.

Any requests to the API will be proxied to the real script when it's loaded.

$script

The $script property is a special property that gives you access to the underlying script instance.

It is a Promise and the script API in one. This means you can await the script to load and use the API directly.

const { $script } = useScript({
  // ...
})

$script
  .then() // script is loaded
  .catch() // script failed to load
  • status

The status of the script. Can be one of the following: 'awaitingLoad' | 'loading' | 'loaded' | 'error'

  • load

Trigger the script to load. This is useful when using the manual loading strategy.

const { $script } = useScript({
  // ...
}, {
  trigger: 'manual'
})
// ...
$script.load()

Examples

CloudFlare Analytics

Unhead
import { useScript } from 'unhead'

interface CloudflareAnalyticsApi {
  __cfBeacon: {
    load: 'single'
    spa: boolean
    token: string
  }
  __cfRl?: unknown
}

declare global {
  interface Window extends CloudflareAnalyticsApi {}
}

export function useCloudflareAnalytics() {
  return useScript<CloudflareAnalyticsApi>({
    'src': 'https://static.cloudflareinsights.com/beacon.min.js',
    'data-cf-beacon': JSON.stringify({ token: 'my-token', spa: true }),
  }, {
    use() {
      return { __cfBeacon: window.__cfBeacon, __cfRl: window.__cfRl }
    },
  })
}

Fathom Analytics

Unhead
import { useScript } from 'unhead'

interface FathomAnalyticsApi {
  trackPageview: (ctx?: { url: string, referrer?: string }) => void
  trackGoal: (eventName: string, value?: { _value: number }) => void
}

declare global {
  interface Window { fathom: FathomAnalyticsApi }
}

export function useFathomAnalytics() {
  return useScript<FathomAnalyticsApi>({
    'src': 'https://cdn.usefathom.com/script.js',
    'data-site': 'my-site',
    // See https://usefathom.com/docs/script/script-advanced
  }, {
    use: () => window.fathom,
  })
}

Google Analytics

Unhead
import { useScript } from 'unhead'

interface GoogleAnalyticsApi {
  gtag: ((fn: 'event', opt: string, opt2: { [key: string]: string }) => void)
}

declare global {
  interface Window extends GoogleAnalyticsApi {}
}

export function useGoogleAnalytics() {
  return useScript<GoogleAnalyticsApi>({
    src: 'https://www.google-analytics.com/analytics.js',
  }, {
    use: () => ({ gtag: window.gtag })
  })
}