Index of every page on this site: llms.txt
TypeScript
Core Concepts

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: true
  • fetchpriority: 'low'
  • crossorigin: 'anonymous' for absolute or protocol-relative URLs
  • referrerpolicy: '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' or undefined: use the integration's normal client loading behavior
  • 'server': add the script during SSR
  • 'manual': wait for an explicit load() call
  • a promise: load when it fulfills with undefined or a truthy value
  • a function: receive load and 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

Did this page help you?