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
orPromise
- 🚀 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.
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 theuseScript
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 therequestIdleCallback
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.
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.
useScript('https://www.google-analytics.com/analytics.js')
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
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
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
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 })
})
}