import * as React from 'react'; export type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error'; // Cached script statuses const cachedScriptStatuses: Record = {}; /** * 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(() => { 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; }