Skip to content

Instantly share code, notes, and snippets.

@SarKurd
Created August 9, 2024 11:29
Show Gist options
  • Select an option

  • Save SarKurd/7521109a6f0eaff157353ef6e0fe8ab6 to your computer and use it in GitHub Desktop.

Select an option

Save SarKurd/7521109a6f0eaff157353ef6e0fe8ab6 to your computer and use it in GitHub Desktop.
Load scripts in React TS
import { useEffect, useState } from 'react'
type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error'
type UseScriptOptions = {
removeOnUnmount?: boolean
id?: string
}
// Cached script statuses
const cachedScriptStatuses = new Map<string, UseScriptStatus | undefined>()
function getScriptNode(src: string) {
const node = document.querySelector<HTMLScriptElement>(`script[src="${src}"]`)
const status = node?.getAttribute('data-status') as
| UseScriptStatus
| undefined
return {
node,
status,
}
}
function useScript(
src?: string,
options?: UseScriptOptions
): {
status: UseScriptStatus
node: HTMLScriptElement | null
} {
const [status, setStatus] = useState<UseScriptStatus>(() => {
if (!src) {
return 'idle'
}
if (typeof window === 'undefined') {
// SSR Handling - always return 'loading'
return 'loading'
}
return cachedScriptStatuses.get(src) ?? 'loading'
})
const [node, setNode] = useState<HTMLScriptElement | null>(null)
useEffect(() => {
if (!src) {
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')
}
setNode(scriptNode)
// 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 (options?.removeOnUnmount) {
scriptNode.remove()
cachedScriptStatuses.delete(src)
}
}
}
}, [src, options?.removeOnUnmount, options?.id])
return { status, node }
}
export default useScript
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment