Skip to content

Commit

Permalink
fix: useScript
Browse files Browse the repository at this point in the history
  • Loading branch information
juliencrn committed Oct 13, 2022
1 parent 44656ac commit dbec137
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 69 deletions.
2 changes: 1 addition & 1 deletion src/useScript/useScript.demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function Component() {
<div>
<p>{`Current status: ${status}`}</p>

{status === 'ok' && <p>You can use the script here.</p>}
{status === 'ready' && <p>You can use the script here.</p>}
</div>
)
}
131 changes: 63 additions & 68 deletions src/useScript/useScript.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { useEffect, useState } from 'react'

export type UseScriptStatus = 'idle' | 'loading' | 'ok' | 'error'
export type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error'
export interface UseScriptOptions {
shouldLoad?: boolean
shouldPreventLoad?: boolean
removeOnUnmount?: boolean
}

const defaultOptions: UseScriptOptions = {
shouldLoad: true,
removeOnUnmount: true,
}

// Cached script statuses
const cachedScriptStatuses: Record<string, UseScriptStatus | undefined> = {}

Expand All @@ -30,11 +25,10 @@ function getScriptNode(src: string) {

function useScript(
src: string | null,
options: UseScriptOptions = defaultOptions,
options?: UseScriptOptions,
): UseScriptStatus {
const { shouldLoad, removeOnUnmount } = options
const [status, setStatus] = useState<UseScriptStatus>(() => {
if (!shouldLoad || !src) {
if (!src || options?.shouldPreventLoad) {
return 'idle'
}

Expand All @@ -46,71 +40,72 @@ function useScript(
return cachedScriptStatuses[src] ?? 'loading'
})

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

const cachedScriptStatus = cachedScriptStatuses[src]
if (cachedScriptStatus === 'ok' || cachedScriptStatus === 'error') {
// If the script is already cached, set its status immediately
setStatus(cachedScriptStatus)
return
}
const cachedScriptStatus = cachedScriptStatuses[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
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' ? 'ok' : '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 ?? cachedScriptStatuses[src] ?? 'loading')
// 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
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)
}

// 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' ? 'ok' : 'error'
setStatus(newStatus)
cachedScriptStatuses[src] = newStatus
}
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[src] = newStatus
}

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

// Remove event listeners on cleanup
return () => {
if (scriptNode && removeOnUnmount) {
scriptNode.removeEventListener('load', setStateFromEvent)
scriptNode.removeEventListener('error', setStateFromEvent)
}
// Remove event listeners on cleanup
return () => {
if (scriptNode) {
scriptNode.removeEventListener('load', setStateFromEvent)
scriptNode.removeEventListener('error', setStateFromEvent)
}
},
[src, shouldLoad, removeOnUnmount], // Only re-run effect if script src changes
)

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

return status
}
Expand Down

0 comments on commit dbec137

Please sign in to comment.