diff --git a/src/useScript/useScript.demo.tsx b/src/useScript/useScript.demo.tsx
index f9382f9e..cf93c20b 100644
--- a/src/useScript/useScript.demo.tsx
+++ b/src/useScript/useScript.demo.tsx
@@ -8,7 +8,9 @@ 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`)
+ const status = useScript(`https://code.jquery.com/jquery-3.5.1.min.js`, {
+ removeOnUnmount: false,
+ })
useEffect(() => {
if (typeof jQuery !== 'undefined') {
@@ -22,7 +24,7 @@ export default function Component() {
{`Current status: ${status}`}
- {status === 'ready' &&
You can use the script here.
}
+ {status === 'ok' &&
You can use the script here.
}
)
}
diff --git a/src/useScript/useScript.ts b/src/useScript/useScript.ts
index 65c8736f..61cfd4eb 100644
--- a/src/useScript/useScript.ts
+++ b/src/useScript/useScript.ts
@@ -1,67 +1,115 @@
import { useEffect, useState } from 'react'
-export type Status = 'idle' | 'loading' | 'ready' | 'error'
-export type ScriptElt = HTMLScriptElement | null
+export type UseScriptStatus = 'idle' | 'loading' | 'ok' | 'error'
+export interface UseScriptOptions {
+ shouldLoad?: boolean
+ removeOnUnmount?: boolean
+}
+
+const defaultOptions: UseScriptOptions = {
+ shouldLoad: true,
+ removeOnUnmount: true,
+}
+
+// Cached script statuses
+const cachedScriptStatuses: Record = {}
+
+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,
+ }
+}
+
+function useScript(
+ src: string | null,
+ options: UseScriptOptions = defaultOptions,
+): UseScriptStatus {
+ const { shouldLoad, removeOnUnmount } = options
+ const [status, setStatus] = useState(() => {
+ if (!shouldLoad || !src) {
+ return 'idle'
+ }
-function useScript(src: string): Status {
- const [status, setStatus] = useState(src ? 'loading' : 'idle')
+ if (typeof window === 'undefined') {
+ // SSR Handling - always return 'loading'
+ return 'loading'
+ }
+
+ return cachedScriptStatuses[src] ?? 'loading'
+ })
useEffect(
() => {
- if (!src) {
- setStatus('idle')
+ if (!src || !shouldLoad) {
+ return
+ }
+
+ const cachedScriptStatus = cachedScriptStatuses[src]
+ if (cachedScriptStatus === 'ok' || 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
- let script: ScriptElt = document.querySelector(`script[src="${src}"]`)
+ const script = getScriptNode(src)
+ let scriptNode = script.node
- if (!script) {
- // Create script
- script = document.createElement('script')
- script.src = src
- script.async = true
- script.setAttribute('data-status', 'loading')
- // Add script to document body
- document.body.appendChild(script)
+ if (!scriptNode) {
+ // Create script element and add it to document body
+ scriptNode = document.createElement('script')
+ scriptNode.src = src
+ scriptNode.async = true
+ 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) => {
- script?.setAttribute(
- 'data-status',
- event.type === 'load' ? 'ready' : 'error',
- )
+ const scriptStatus: UseScriptStatus =
+ event.type === 'load' ? 'ok' : 'error'
+
+ scriptNode?.setAttribute('data-status', scriptStatus)
}
- script.addEventListener('load', setAttributeFromEvent)
- script.addEventListener('error', setAttributeFromEvent)
+ scriptNode.addEventListener('load', setAttributeFromEvent)
+ scriptNode.addEventListener('error', setAttributeFromEvent)
} else {
// Grab existing script status from attribute and set to state.
- setStatus(script.getAttribute('data-status') as Status)
+ setStatus(script.status ?? cachedScriptStatuses[src] ?? '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) => {
- setStatus(event.type === 'load' ? 'ready' : 'error')
+ const newStatus = event.type === 'load' ? 'ok' : 'error'
+ setStatus(newStatus)
+ cachedScriptStatuses[src] = newStatus
}
// Add event listeners
- script.addEventListener('load', setStateFromEvent)
- script.addEventListener('error', setStateFromEvent)
+ scriptNode.addEventListener('load', setStateFromEvent)
+ scriptNode.addEventListener('error', setStateFromEvent)
// Remove event listeners on cleanup
return () => {
- if (script) {
- script.removeEventListener('load', setStateFromEvent)
- script.removeEventListener('error', setStateFromEvent)
+ if (scriptNode && removeOnUnmount) {
+ scriptNode.removeEventListener('load', setStateFromEvent)
+ scriptNode.removeEventListener('error', setStateFromEvent)
}
}
},
- [src], // Only re-run effect if script src changes
+ [src, shouldLoad, removeOnUnmount], // Only re-run effect if script src changes
)
return status