Skip to content

Instantly share code, notes, and snippets.

@zavan
Last active November 19, 2022 17:13
Show Gist options
  • Save zavan/42f394491c17701d8f68dcc773b58a9f to your computer and use it in GitHub Desktop.
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)
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;
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;
// 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