Script Loading
useScript() manages an external script's lifecycle and returns a shared script instance.
import { useScript } from 'unhead'
const script = useScript(unheadInstance, 'https://example.com/sdk.js')
script.onLoaded(() => console.log('SDK loaded'))
script.onError(error => console.error('SDK failed', error))
Script identity
Scripts are shared within one Unhead instance. The identity is the input's key, then its src, then a string innerHTML value. Repeated calls with the same identity return the cached ScriptInstance and do not add another script element.
useScript(unheadInstance, 'https://example.com/sdk.js')
useScript(unheadInstance, 'https://example.com/sdk.js') // same instance in this app/request
The cache is not process-global. Separate server requests or client apps with separate head instances do not share it.
Default attributes
When load() adds the script entry, Unhead supplies:
defer: truefetchpriority: 'low'crossorigin: 'anonymous'for absolute or protocol-relative URLsreferrerpolicy: 'no-referrer'for absolute or protocol-relative URLs
Input attributes override these defaults. Unhead does not add async by default.
Loading triggers
The trigger option accepts:
'client'orundefined: use the integration's normal client loading behavior'server': add the script during SSR'manual': wait for an explicitload()call- a promise: load when it fulfills with
undefinedor a truthy value - a function: receive
loadand optionally return a cleanup function
import { useScript } from 'unhead'
const manual = useScript(unheadInstance, 'https://example.com/heavy-sdk.js', {
trigger: 'manual'
})
async function enableFeature() {
const api = await manual.load()
if (!api)
return // Loading failed, or the script was removed.
}
useScript(unheadInstance, 'https://example.com/video-player.js', {
trigger: (load) => {
const element = document.querySelector('#video-container')
element?.addEventListener('click', load, { once: true })
return () => element?.removeEventListener('click', load)
}
})
Framework integrations schedule the default trigger according to their mount lifecycle. Use an explicit trigger when timing must be identical across integrations.
Resolving a script API
Provide resolve to tell Unhead how to obtain the API exposed by the script. The resolver runs on the client and may return a value or a promise.
import { useScript } from 'unhead'
interface ExampleSdk {
track: (event: string, data?: Record<string, unknown>) => void
}
const sdk = useScript<ExampleSdk>(unheadInstance, 'https://example.com/sdk.js', {
resolve: () => window.exampleSdk
})
sdk.onLoaded((api) => {
api.track('page_view')
})
The legacy use option also resolves an API, but resolve additionally receives an AbortSignal and waitFor() helper for SDKs that become ready through a callback.
Calling Through the Proxy
When resolve or use is configured, proxy records property access and function calls made before the API is ready. It replays function calls in order after resolution.
const sdk = useScript<ExampleSdk>(unheadInstance, 'https://example.com/sdk.js', {
resolve: () => window.exampleSdk
})
sdk.proxy.track('page_view')
Proxy functions intentionally return void, so they cannot provide synchronous results. Calls are never replayed if loading or API resolution fails. Without resolve or use, no API proxy is created; use onLoaded() only to observe the element load.
Resource warmup
warmupStrategy accepts false, 'preload', 'preconnect', or 'dns-prefetch'.
These hints do different amounts of work: dns-prefetch resolves an origin, preconnect also opens its network connection, and preload fetches a specific resource for the current navigation. Use the least expensive hint that matches what you know, and reserve preconnects for origins the page will use soon. See MDN's resource-hint guide and web.dev's resource-hint guidance.
useScript(unheadInstance, 'https://example.com/sdk.js', {
trigger: 'manual',
warmupStrategy: 'preconnect'
})
You can also add a hint later:
const script = useScript(unheadInstance, 'https://example.com/sdk.js', {
trigger: 'manual',
warmupStrategy: false
})
script.warmup('preconnect')
await script.load()
preconnect and dns-prefetch are ignored for root-relative sources. A preload hint includes as="script".
Removing a Script
script.remove() disposes the managed script entry, removes its warmup entry, aborts pending lifecycle work, changes the status to removed, and deletes it from the head's script cache. A later call with the same source creates a new instance.
Most framework integrations do not remove an already loaded shared script when a component unmounts. Use scope: true in integrations that expose script scopes when you need consumer-owned callbacks and triggers, then call dispose() to release that scope without removing the shared script.
See Also
- useScript() API: Complete types and lifecycle
- DOM Event Handling: Direct resource handlers