useScript
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 googleAnalytics = useScript('https://www.google-analytics.com/analytics.js', {
beforeInit() {
window.dataLayer = window.dataLayer || []
window.dataLayer.push('js', new Date())
window.dataLayer.push('config', options.id)
},
use() { return { dataLayer: window.dataLayer } }
})
// fully typed, usable in SSR and when lazy loaded
googleAnalytics.proxy.dataLayer.push('event', 'page_view', {
page_title: 'Home',
page_location: 'https://example.com',
page_path: '/',
})
declare global {
interface Window {
dataLayer: any[]
}
}
Usage
referrerpolicy
and crossorigin
The useScript
composable is optimized for end user privacy and security.
By default, the referrerpolicy
is set to no-referrer
and crossorigin
is set to anonymous
.
Some scripts will not run correctly with these settings. If you find yourself having CORS or behavior issues, you should disable these defaults.
const instance = useScript({
src: 'https://example.com/my-awesome-script.js',
// setting these to false will revert the defaults
referrerpolicy: false,
crossorigin: false,
}, {
use() {
return window.myAwesomeScript
}
})
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 theload()
function. 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 { load } = useScript('/script.js', {
trigger: 'manual'
})
// ...
load((instance) => {
// use the script instance
})
Waiting for Script Load
To use the underlying API exposed by a script, it's recommended to use the onLoaded
function, which accepts
a callback function once the script is loaded.
const { onLoaded } = useScript('/script.js')
onLoaded(() => {
// script ready!
})
const { onLoaded } = useScript('/script.js')
onLoaded(() => {
// script ready!
})
If you have registered your script using a manual
trigger, then you can call load()
with the same syntax.
const { load } = useScript('/script.js', {
trigger: 'manual'
})
load((instance) => {
// runs once the script loads
})
The onLoaded
function returns a function that you can use to dispose of the callback. For reactive integrations
such as Vue, this will automatically bind to the scope lifecycle.
const { onLoaded } = useScript('/script.js')
const dispose = onLoaded(() => {
// script ready!
})
// ...
dispose() // nevermind!
const { onLoaded } = useScript('/script.js')
onLoaded(() => {
// this will never be called once the scope unmounts
})
If you just need to call a function on a script once it's loaded and don't care about the result, you may consider using the Proxy API instead.
Removing a Script
When you're done with a script, you can remove it from the document using the remove()
function.
const myScript = useScript('/script.js')
myScript.remove()
The remove()
function will return a boolean indicating if the script was removed in the case where the
script has already been removed.
Script Loading Errors
Sometimes scripts just won't load, this can be due to network issues, browser extensions blocking the script or many other reasons.
As the script instance is a native promise, you can use the .catch()
function.
const myScript = useScript('/script.js')
.onError((err) => {
console.error('Failed to load script', err)
})
Otherwise, you always check the status of the script using status
.
const myScript = useScript('/script.js')
myScript.status // 'awaitingLoad' | 'loading' | 'loaded' | 'error'
const myScript = useScript('/script.js')
myScript.status // Ref<'awaitingLoad' | 'loading' | 'loaded' | 'error'>
Proxy API
A proxy
object is accessible on the script instance which provides a consistent interface for calling script functions
regardless of the script being loaded.
This can be useful in instances where you don't care when the function is called or what it returns.
declare global {
interface Window {
analytics: {
event: ((arg: string) => void)
}
}
}
const analytics = useScript('/analytics.js', {
use() { return window.analytics }
})
// send an event if or when the script is loaded
analytics.proxy.event('foo') // void
Using the proxy API will noop in SSR, is stubbable and is future-proofed to call functions of scripts through web workers.
It's important to know when to and not to use the proxy API, it should not be used for accessing properties or when you need to know the return of the function.
declare global {
interface Window {
analytics: {
event: ((arg: string) => void)
siteId: number
loadUser: () => { id: string }
}
}
}
const analytics = useScript('/analytics.js', {
use() { return window.analytics }
})
const val = myScript.proxy.siteId // ❌ val will be a function
const user = myScript.proxy.loadUser() // ❌ the result of calling any function is always void
Stubbing
In cases where you're using the Proxy API, you can additionally hook into the resolving of the proxy using the stub
option.
For example, in a server context, we probably want to polyfill some returns so our scrits remains functional.
const analytics = useScript<{ event: ((arg: string) => boolean) }>('/analytics.js', {
use() { return window.analytics },
stub() {
if (import.meta.server) {
return {
event: (e) => {
console.log('event', e)
}
}
}
}
})
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
trigger
- Type:
'undefined' | 'manual' | 'server' | 'client' | Promise<void>
- Additional Vue Types:
Ref<boolean>
A strategy to use for when the script should be loaded. Defaults to client
.
useScript({
src: 'https://example.com/script.js',
}, {
trigger: new Promise((resolve) => {
setTimeout(resolve, 10000) // load after 10 seconds
})
})
When server
is set as the trigger, the script will be injected into the SSR HTML response, allowing for quicker
loading of the script.
The client
trigger ensures that the script is only loaded when the script is hydrated on the client-side.
use
- Type:
() => API
A function that resolves the scripts API. This is only called client-side.
const fathom = useScript<FathomApi>({
// fathom analytics
src: 'https://cdn.usefathom.com/script.js',
}, {
use: () => window.fathom
})
fathom.then((api) => {
// api is equal to window.fathom
})
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 myScript = 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 })
}
})
const { sendEvent, doSomething } = myScript.proxy
// 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()
Script Instance API
The useScript
composable returns the script instance that you can use to interact with the script.
id
The unique ID of the script instance.
status
The status of the script. Can be one of the following: 'awaitingLoad' | 'loading' | 'loaded' | 'error'
In Vue, this is a Ref
.
onLoaded(cb: (instance: ReturnType) => void | Promise): () => void
A function that is called when the script is loaded. This is useful when you want to access the script directly.
const myScript = useScript('/script.js')
myScript.onLoaded(() => {
// ready
})
then(cb: (instance: ReturnType) => void | Promise)
A function that is called when the script is loaded. This is useful when you want to access the script directly.
const myScript = useScript('/script.js')
myScript.onLoaded(() => {
// ready
})
load(callback?: (instance: ReturnType) => void | Promise): Promise<ReturnType>
Trigger the script to load. This is useful when using the manual
loading strategy.
const { load } = useScript('/script.js', {
trigger: 'manual'
})
// ...
load()
You can optionally provide a callback function to run once the script is loaded, this is recommended over using load()
as a promise.
const { load } = useScript('/script.js', {
trigger: 'manual'
})
load(() => {
})
remove()
Remove the script from the document.
proxy
The proxy API for calling the script functions. See the Proxy API for further details.
const myScript = useScript<MyScriptApi>('/script.js', {
use() { return window.myScript }
})
myScript.proxy.myFunction('hello')
instance
Internal value providing the use()
function, this will be the result. This is passed when resolving the script using then()
or load()
.
const myScript = useScript<MyScriptApi>('/script.js', {
use() { return window.myScript }
})
myScript.instance // window.myScript
entry
The internal head entry for the script. This is useful for debugging and tracking the script.
const myScript = useScript('/script.js')
myScript.entry // ReturnType<typeof useHead>
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 })
})
}