Skip to content

Commit c884186

Browse files
committed
Improved useScript hook
1 parent 7575c64 commit c884186

File tree

1 file changed

+2
-29
lines changed

1 file changed

+2
-29
lines changed

src/pages/useScript.md

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,7 @@ links:
1111
- url: https://github.com/palmerhq/the-platform#usescript
1212
name: useScript from palmerhq/the-platform
1313
description: Similar hook but returns a promise for use with React Suspense.
14-
code:
15-
"import { useState, useEffect } from 'react';\r\n\r\n// Usage\r\nfunction App()
16-
{\r\n const [loaded, error] = useScript(\r\n 'https://pm28k14qlj.codesandbox.io/test-external-script.js'\r\n
17-
\ );\r\n\r\n return (\r\n <div>\r\n <div>\r\n Script loaded: <b>{loaded.toString()}</b>\r\n
18-
\ </div>\r\n {loaded && !error && (\r\n <div>\r\n Script
19-
function call response: <b>{TEST_SCRIPT.start()}</b>\r\n </div>\r\n )}\r\n
20-
\ </div>\r\n );\r\n}\r\n\r\n// Hook\r\nlet cachedScripts = [];\r\nfunction useScript(src)
21-
{\r\n // Keeping track of script loaded and error state\r\n const [state, setState]
22-
= useState({\r\n loaded: false,\r\n error: false\r\n });\r\n\r\n useEffect(\r\n
23-
\ () => {\r\n // If cachedScripts array already includes src that means another
24-
instance ...\r\n // ... of this hook already loaded this script, so no need
25-
to load again.\r\n if (cachedScripts.includes(src)) {\r\n setState({\r\n
26-
\ loaded: true,\r\n error: false\r\n });\r\n } else
27-
{\r\n cachedScripts.push(src);\r\n\r\n // Create script\r\n let
28-
script = document.createElement('script');\r\n script.src = src;\r\n script.async
29-
= true;\r\n\r\n // Script event listener callbacks for load and error\r\n
30-
\ const onScriptLoad = () => {\r\n setState({\r\n loaded:
31-
true,\r\n error: false\r\n });\r\n };\r\n\r\n const
32-
onScriptError = () => {\r\n // Remove from cachedScripts we can try loading
33-
again\r\n const index = cachedScripts.indexOf(src);\r\n if (index
34-
>= 0) cachedScripts.splice(index, 1);\r\n script.remove();\r\n\r\n setState({\r\n
35-
\ loaded: true,\r\n error: true\r\n });\r\n };\r\n\r\n
36-
\ script.addEventListener('load', onScriptLoad);\r\n script.addEventListener('error',
37-
onScriptError);\r\n\r\n // Add script to document body\r\n document.body.appendChild(script);\r\n\r\n
38-
\ // Remove event listeners on cleanup\r\n return () => {\r\n script.removeEventListener('load',
39-
onScriptLoad);\r\n script.removeEventListener('error', onScriptError);\r\n
40-
\ };\r\n }\r\n },\r\n [src] // Only re-run effect if script src
41-
changes\r\n );\r\n\r\n return [state.loaded, state.error];\r\n}"
14+
code: "import { useState, useEffect } from 'react';\r\n\r\n\/\/ Usage\r\nfunction App() {\r\n const status = useScript(\r\n 'https:\/\/pm28k14qlj.codesandbox.io\/test-external-script.js'\r\n );\r\n\r\n return (\r\n <div>\r\n <div>\r\n Script status: <b>{status}<\/b>\r\n <\/div>\r\n {status === \"ready\" && (\r\n <div>\r\n Script function call response: <b>{TEST_SCRIPT.start()}<\/b>\r\n <\/div>\r\n )}\r\n <\/div>\r\n );\r\n}\r\n\r\n\/\/ Hook\r\nfunction useScript(src) {\r\n \/\/ Keep track of script status (\"idle\", \"loading\", \"ready\", \"error\")\r\n const [status, setStatus] = useState(src ? \"loading\" : \"idle\");\r\n\r\n useEffect(\r\n () => {\r\n \/\/ Allow falsy src value if waiting on other data needed for\r\n \/\/ constructing the script URL passed to this hook.\r\n if (!src) {\r\n setStatus(\"idle\");\r\n return;\r\n }\r\n\r\n \/\/ Fetch existing script element by src\r\n \/\/ It may have been added by another intance of this hook\r\n let script = document.querySelector(`script[src=\"${src}\"]`);\r\n\r\n if (!script) {\r\n \/\/ Create script\r\n script = document.createElement(\"script\");\r\n script.src = src;\r\n script.async = true;\r\n script.setAttribute(\"data-status\", \"loading\");\r\n \/\/ Add script to document body\r\n document.body.appendChild(script);\r\n\r\n \/\/ Store status in attribute on script\r\n \/\/ This can be read by other instances of this hook\r\n const setAttributeFromEvent = (event) => {\r\n script.setAttribute(\r\n \"data-status\",\r\n event.type === \"load\" ? \"ready\" : \"error\"\r\n );\r\n };\r\n\r\n script.addEventListener(\"load\", setAttributeFromEvent);\r\n script.addEventListener(\"error\", setAttributeFromEvent);\r\n } else {\r\n \/\/ Grab existing script status from attribute and set to state.\r\n setStatus(script.getAttribute(\"data-status\"));\r\n }\r\n\r\n \/\/ Script event handler to update status in state\r\n \/\/ Note: Even if the script already exists we still need to add\r\n \/\/ event handlers to update the state for *this* hook instance.\r\n const setStateFromEvent = (event) => {\r\n setStatus(event.type === \"load\" ? \"ready\" : \"error\");\r\n };\r\n\r\n \/\/ Add event listeners\r\n script.addEventListener(\"load\", setStateFromEvent);\r\n script.addEventListener(\"error\", setStateFromEvent);\r\n\r\n \/\/ Remove event listeners on cleanup\r\n return () => {\r\n if (script) {\r\n script.removeEventListener(\"load\", setStateFromEvent);\r\n script.removeEventListener(\"error\", setStateFromEvent);\r\n }\r\n };\r\n },\r\n [src] \/\/ Only re-run effect if script src changes\r\n );\r\n\r\n return status;\r\n}"
4215
---
4316

44-
This hook makes it super easy to dynamically load an external script and know when its loaded. This is useful when you need to interact with a 3rd party library (Stripe, Google Analytics, etc) and you'd prefer to load the script when needed rather then include it in the document head for every page request. In the example below we wait until the script has loaded successfully before calling a function declared in the script. If you're interested in seeing how this would look if implemented as a Higher Order Component then check out the [source of react-script-loader-hoc](https://github.com/sesilio/react-script-loader-hoc/blob/master/src/index.js). I personally find it much more readable as a hook. Another advantage is because it's so easy to call the same hook multiple times to load multiple different scripts, unlike the HOC implementation, we can skip adding support for passing in multiple src strings.
17+
This hook makes it super easy to dynamically load an external script and know when its loaded. This is useful when you need to interact with a 3rd party library (Stripe, Google Analytics, etc) and you'd prefer to load the script when needed rather then include it in the document head for every page request. In the example below we wait until the script has loaded successfully before calling a function declared in the script. If you're interested in seeing how this would look if implemented as a Higher Order Component then check out the [source of react-script-loader-hoc](https://github.com/sesilio/react-script-loader-hoc/blob/master/src/index.js). I personally find it much more readable as a hook. Another advantage is because you can use this hook multiple times within a component, we don't need to add support for loading multiple scripts and we can keep our hook logic nice and simple.

0 commit comments

Comments
 (0)