useScript


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

Usage

import { useEffect } from 'react'

import { useScript } from 'usehooks-ts'

// it's an example, use your types instead
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
      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
      alert(jQuery.fn.jquery)
    }
  }, [status])

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

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

API

useScript(src, options?): UseScriptStatus

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

Parameters

NameTypeDescription
srcnull | stringThe source URL of the script to load. Set to null or omit to prevent loading (optional).
options?UseScriptOptionsAdditional options for controlling script loading (optional).

Returns

UseScriptStatus

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

Type aliases

Ƭ UseScriptOptions: Object

Hook options.

Type declaration

NameTypeDescription
id?stringScript's id (optional).
removeOnUnmount?booleanIf true, removes the script from the DOM when the component unmounts (optional).
shouldPreventLoad?booleanIf true, prevents the script from being loaded (optional).

Ƭ UseScriptStatus: "idle" | "loading" | "ready" | "error"

Script loading status.

Hook

import { useEffect, useState } from 'react'

type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error'

type UseScriptOptions = {
  shouldPreventLoad?: boolean
  removeOnUnmount?: boolean
  id?: string
}

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

function getScriptNode(src: string) {
  const node: HTMLScriptElement | null = document.querySelector(
    `script[src="${src}"]`,
  )
  const status = node?.getAttribute('data-status') as
    | UseScriptStatus
    | undefined

  return {
    node,
    status,
  }
}

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
}