useScript()

Dynamically load an external script in one line with this React hook. This can be useful to integrate a third party library like Google Analytics or Stripe.

This avoids loading this script in the <head> </head> on all your pages if it is not necessary.

The Hook

1import { useEffect, useState } from 'react'
2
3export type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error'
4export interface UseScriptOptions {
5 shouldPreventLoad?: boolean
6 removeOnUnmount?: boolean
7}
8
9// Cached script statuses
10const cachedScriptStatuses: Record<string, UseScriptStatus | undefined> = {}
11
12function getScriptNode(src: string) {
13 const node: HTMLScriptElement | null = document.querySelector(
14 `script[src="${src}"]`,
15 )
16 const status = node?.getAttribute('data-status') as
17 | UseScriptStatus
18 | undefined
19
20 return {
21 node,
22 status,
23 }
24}
25
26function useScript(
27 src: string | null,
28 options?: UseScriptOptions,
29): UseScriptStatus {
30 const [status, setStatus] = useState<UseScriptStatus>(() => {
31 if (!src || options?.shouldPreventLoad) {
32 return 'idle'
33 }
34
35 if (typeof window === 'undefined') {
36 // SSR Handling - always return 'loading'
37 return 'loading'
38 }
39
40 return cachedScriptStatuses[src] ?? 'loading'
41 })
42
43 useEffect(() => {
44 if (!src || options?.shouldPreventLoad) {
45 return
46 }
47
48 const cachedScriptStatus = cachedScriptStatuses[src]
49 if (cachedScriptStatus === 'ready' || cachedScriptStatus === 'error') {
50 // If the script is already cached, set its status immediately
51 setStatus(cachedScriptStatus)
52 return
53 }
54
55 // Fetch existing script element by src
56 // It may have been added by another instance of this hook
57 const script = getScriptNode(src)
58 let scriptNode = script.node
59
60 if (!scriptNode) {
61 // Create script element and add it to document body
62 scriptNode = document.createElement('script')
63 scriptNode.src = src
64 scriptNode.async = true
65 scriptNode.setAttribute('data-status', 'loading')
66 document.body.appendChild(scriptNode)
67
68 // Store status in attribute on script
69 // This can be read by other instances of this hook
70 const setAttributeFromEvent = (event: Event) => {
71 const scriptStatus: UseScriptStatus =
72 event.type === 'load' ? 'ready' : 'error'
73
74 scriptNode?.setAttribute('data-status', scriptStatus)
75 }
76
77 scriptNode.addEventListener('load', setAttributeFromEvent)
78 scriptNode.addEventListener('error', setAttributeFromEvent)
79 } else {
80 // Grab existing script status from attribute and set to state.
81 setStatus(script.status ?? cachedScriptStatus ?? 'loading')
82 }
83
84 // Script event handler to update status in state
85 // Note: Even if the script already exists we still need to add
86 // event handlers to update the state for *this* hook instance.
87 const setStateFromEvent = (event: Event) => {
88 const newStatus = event.type === 'load' ? 'ready' : 'error'
89 setStatus(newStatus)
90 cachedScriptStatuses[src] = newStatus
91 }
92
93 // Add event listeners
94 scriptNode.addEventListener('load', setStateFromEvent)
95 scriptNode.addEventListener('error', setStateFromEvent)
96
97 // Remove event listeners on cleanup
98 return () => {
99 if (scriptNode) {
100 scriptNode.removeEventListener('load', setStateFromEvent)
101 scriptNode.removeEventListener('error', setStateFromEvent)
102 }
103
104 if (scriptNode && options?.removeOnUnmount) {
105 scriptNode.remove()
106 }
107 }
108 }, [src, options?.shouldPreventLoad, options?.removeOnUnmount])
109
110 return status
111}
112
113export default useScript

Usage

1import { useEffect } from 'react'
2
3import { useScript } from 'usehooks-ts'
4
5// it's an example, use your types instead
6// eslint-disable-next-line @typescript-eslint/no-explicit-any
7declare const jQuery: any
8
9export default function Component() {
10 // Load the script asynchronously
11 const status = useScript(`https://code.jquery.com/jquery-3.5.1.min.js`, {
12 removeOnUnmount: false,
13 })
14
15 useEffect(() => {
16 if (typeof jQuery !== 'undefined') {
17 // jQuery is loaded => print the version
18 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
19 alert(jQuery.fn.jquery)
20 }
21 }, [status])
22
23 return (
24 <div>
25 <p>{`Current status: ${status}`}</p>
26
27 {status === 'ready' && <p>You can use the script here.</p>}
28 </div>
29 )
30}

Edit on CodeSandbox

See a way to make this page better?
Edit there »