Skip to main content

All hooks

useScript

Custom hook that dynamically loads scripts and tracking their loading status.


This avoids loading this script in the <head> </head> on all your pages if it is not necessary.

Usage

import { useEffect } from 'react'

import { useScript } from './useScript'

// it's an example, use your types instead
// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare const jQuery: any

export default function Component() {
  // Load the script asynchronously
  const status = useScript(`https://code.jquery.com/jquery-3.5.1.min.js`, {
    removeOnUnmount: false,
    id: 'jquery',
  })

  useEffect(() => {
    if (typeof jQuery !== 'undefined') {
      // jQuery is loaded => print the version

      alert(jQuery.fn.jquery)
    }
  }, [status])

  return (
    <div>
      <p>{`Current status: ${status}`}</p>

      {status === 'ready' && <p>You can use the script here.</p>}
    </div>
  )
}

API

function useScript(src: string | null, options?: UseScriptOptions): UseScriptStatus

Custom hook that dynamically loads scripts and tracking their loading status.

Parameters

NameTypeDefault valueDescription
src`stringnull`-
options?UseScriptOptions-Additional options for controlling script loading (optional).

Returns

The status of the script loading, which can be one of 'idle', 'loading', 'ready', or 'error'.

Type declaration

UseScriptOptions

Hook options.

NameTypeDescription
idstringScript's id (optional).
removeOnUnmountbooleanIf true, removes the script from the DOM when the component unmounts (optional).
shouldPreventLoadbooleanIf true, prevents the script from being loaded (optional).

UseScriptStatus

Script loading status.

Hook

import { useEffect, useState } from 'react'

/** Script loading status. */
export type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error'

/** Hook options. */
export type UseScriptOptions = {
  /** If `true`, prevents the script from being loaded (optional). */
  shouldPreventLoad?: boolean
  /** If `true`, removes the script from the DOM when the component unmounts (optional). */
  removeOnUnmount?: boolean
  /** Script's `id` (optional). */
  id?: string
}

// Cached script statuses
const cachedScriptStatuses = new Map<string, UseScriptStatus | undefined>()

/**
 * Gets the script element with the specified source URL.
 * @param {string} src - The source URL of the script to get.
 * @returns {{ node: HTMLScriptElement | null, status: UseScriptStatus | undefined }} The script element and its loading status.
 * @public
 * @example
 * ```tsx
 * const script = getScriptNode(src);
 * ```
 */
function getScriptNode(src: string) {
  const node: HTMLScriptElement | null = document.querySelector(`script[src="${CSS.escape(src)}"]`)
  const status = node?.getAttribute('data-status') as UseScriptStatus | undefined

  return {
    node,
    status,
  }
}

/**
 * Custom hook that dynamically loads scripts and tracking their loading status.
 * @param {string | null} src - The source URL of the script to load. Set to `null` or omit to prevent loading (optional).
 * @param {UseScriptOptions} [options] - Additional options for controlling script loading (optional).
 * @returns {UseScriptStatus} The status of the script loading, which can be one of 'idle', 'loading', 'ready', or 'error'.
 * @see [Documentation](https://usehooks-ts.com/react-hook/use-script)
 * @example
 * const scriptStatus = useScript('https://example.com/script.js', { removeOnUnmount: true });
 * // Access the status of the script loading (e.g., 'loading', 'ready', 'error').
 */
export function useScript(src: string | null, options?: UseScriptOptions): UseScriptStatus {
  const [status, setStatus] = useState<UseScriptStatus>(() => {
    if (!src || options?.shouldPreventLoad) {
      return 'idle'
    }

    if (typeof window === 'undefined') {
      // SSR Handling - always return 'loading'
      return 'loading'
    }

    return cachedScriptStatuses.get(src) ?? 'loading'
  })

  useEffect(() => {
    if (!src || options?.shouldPreventLoad) {
      return
    }

    const cachedScriptStatus = cachedScriptStatuses.get(src)
    if (cachedScriptStatus === 'ready' || cachedScriptStatus === 'error') {
      // If the script is already cached, set its status immediately
      setStatus(cachedScriptStatus)
      return
    }

    // Fetch existing script element by src
    // It may have been added by another instance of this hook
    const script = getScriptNode(src)
    let scriptNode = script.node

    if (!scriptNode) {
      // Create script element and add it to document body
      scriptNode = document.createElement('script')
      scriptNode.src = src
      scriptNode.async = true
      if (options?.id) {
        scriptNode.id = options.id
      }
      scriptNode.setAttribute('data-status', 'loading')
      document.body.appendChild(scriptNode)

      // Store status in attribute on script
      // This can be read by other instances of this hook
      const setAttributeFromEvent = (event: Event) => {
        const scriptStatus: UseScriptStatus = event.type === 'load' ? 'ready' : 'error'

        scriptNode?.setAttribute('data-status', scriptStatus)
      }

      scriptNode.addEventListener('load', setAttributeFromEvent)
      scriptNode.addEventListener('error', setAttributeFromEvent)
    } else {
      // Grab existing script status from attribute and set to state.
      setStatus(script.status ?? cachedScriptStatus ?? 'loading')
    }

    // Script event handler to update status in state
    // Note: Even if the script already exists we still need to add
    // event handlers to update the state for *this* hook instance.
    const setStateFromEvent = (event: Event) => {
      const newStatus = event.type === 'load' ? 'ready' : 'error'
      setStatus(newStatus)
      cachedScriptStatuses.set(src, newStatus)
    }

    // Add event listeners
    scriptNode.addEventListener('load', setStateFromEvent)
    scriptNode.addEventListener('error', setStateFromEvent)

    // Remove event listeners on cleanup
    return () => {
      if (scriptNode) {
        scriptNode.removeEventListener('load', setStateFromEvent)
        scriptNode.removeEventListener('error', setStateFromEvent)
      }

      if (scriptNode && options?.removeOnUnmount) {
        scriptNode.remove()
        cachedScriptStatuses.delete(src)
      }
    }
  }, [src, options?.shouldPreventLoad, options?.removeOnUnmount, options?.id])

  return status
}