Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
import * as React from 'react';
|
|
|
|
export type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
|
|
// Cached script statuses
|
|
const cachedScriptStatuses: Record<string, UseScriptStatus | undefined> = {};
|
|
|
|
/**
|
|
* Simplified version of https://usehooks-ts.com/react-hook/use-script
|
|
*/
|
|
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 default function useScript(src: string): UseScriptStatus {
|
|
const [status, setStatus] = React.useState<UseScriptStatus>(() => {
|
|
if (typeof window === 'undefined') {
|
|
// SSR Handling - always return 'loading'
|
|
return 'loading';
|
|
}
|
|
|
|
return cachedScriptStatuses[src] ?? 'loading';
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
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' ? '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[src] = newStatus;
|
|
};
|
|
|
|
// Add event listeners
|
|
scriptNode.addEventListener('load', setStateFromEvent);
|
|
scriptNode.addEventListener('error', setStateFromEvent);
|
|
|
|
// Remove event listeners on cleanup
|
|
// eslint-disable-next-line consistent-return
|
|
return () => {
|
|
if (scriptNode) {
|
|
scriptNode.removeEventListener('load', setStateFromEvent);
|
|
scriptNode.removeEventListener('error', setStateFromEvent);
|
|
}
|
|
|
|
if (scriptNode) {
|
|
try {
|
|
scriptNode.remove();
|
|
} catch (error) {
|
|
// ignore error
|
|
}
|
|
}
|
|
};
|
|
}, [src]);
|
|
|
|
return status;
|
|
}
|