Last active
November 19, 2022 17:13
-
-
Save zavan/42f394491c17701d8f68dcc773b58a9f to your computer and use it in GitHub Desktop.
Inject and use global script libs dynamically and idempotently (bonus: react custom hook)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const scripts = {}; | |
const defaultOptions = { | |
async: true, | |
defer: true, | |
}; | |
function getGlobalLib(name, url, options = defaultOptions) { | |
if (scripts[name]) return scripts[name]; | |
if (window[name]) { | |
scripts[name] = window[name]; | |
return scripts[name]; | |
} | |
const mergedOptions = { ...defaultOptions, ...options }; | |
const promise = new Promise((resolve, reject) => { | |
let script = document.createElement("script"); | |
script.async = mergedOptions.async; | |
script.defer = mergedOptions.defer; | |
function onloadHander(_, isAbort) { | |
if ( | |
isAbort || | |
!script.readyState || | |
/loaded|complete/.test(script.readyState) | |
) { | |
script.onload = null; | |
script.onreadystatechange = null; | |
if (isAbort) { | |
reject(); | |
} else { | |
scripts[name] = window[name]; | |
resolve(scripts[name]); | |
} | |
document.body.removeChild(script); | |
script = undefined; | |
} | |
} | |
// It's important that the script is appended before onload and src are set. | |
document.body.appendChild(script); | |
script.onload = script.onreadystatechange = onloadHander; | |
script.src = url; | |
}); | |
scripts[name] = promise; | |
return scripts[name]; | |
} | |
export default getGlobalLib; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useEffect, useState } from "react"; | |
import getGlobalLib from "./getGlobalLib"; | |
// Using an object directly as the function arg causes infinite re-renders. | |
const defaultOptions = {}; | |
function useGlobalLib(name, url, options = defaultOptions) { | |
const [loading, setLoading] = useState(true); | |
const [error, setError] = useState(false); | |
const [lib, setLib] = useState({}); | |
// Options object has to be detructured or we get an infinite loop | |
// due to object referential inequality in useEffect deps. | |
const { async, defer } = options; | |
useEffect(() => { | |
async function getLib() { | |
try { | |
// How to use the module: | |
const lib = await getGlobalLib(name, url, { async, defer }); | |
// lib has to be in an object because it may be a function | |
// which react will think is a callback to execute immediatelly. | |
setLib({ [name]: lib }); | |
} catch (_e) { | |
setError(true); | |
} finally { | |
setLoading(false); | |
} | |
} | |
getLib(); | |
}, [name, url, async, defer]); | |
return { loading, error, [name]: lib[name] }; | |
} | |
export default useGlobalLib; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Example of using the the custom hook to load recaptcha. | |
import useGlobalLib from "./useGlobalLib"; | |
const recaptchaURL = "https://www.google.com/recaptcha/api.js"; | |
function useRecaptcha(action) { | |
const { grecaptcha, loading, error } = useGlobalLib( | |
"grecaptcha", | |
recaptchaURL | |
); | |
if (error) return { error, loading: false }; | |
if (loading) return { loading: true, error: false }; | |
const getRecaptchaToken = () => { | |
// Use the lib! | |
// grecaptcha.execute... | |
}; | |
return { loading: false, error: false, getRecaptchaToken }; | |
} | |
export default useRecaptcha; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment