From e4720169ea711f2feb3b5d1d6bb282a7218f5bcf Mon Sep 17 00:00:00 2001 From: Steve Matney Date: Thu, 7 Jan 2021 08:46:24 -0700 Subject: [PATCH 1/2] Update: utilizing promises for loading scripts --- .eslintrc.js | 2 +- scriptloader-support.ts | 1 + src/hooks/useScriptLoader.ts | 134 ++++++----------------------- src/scriptloader-support/index.tsx | 95 ++++++++++++++++++++ tsconfig.json | 7 +- 5 files changed, 128 insertions(+), 111 deletions(-) create mode 100644 scriptloader-support.ts create mode 100644 src/scriptloader-support/index.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 386997f..36dc00b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,7 +31,7 @@ module.exports = { extends: baseExtends, ignorePatterns: ["dist/**/*"], env: { es6: true }, - parserOptions: { ecmaVersion: 2017 }, + parserOptions: { ecmaVersion: 2021, sourceType: "module" }, overrides: [ { files: ["src/**/*", "./index.ts", "./jest.config.ts"], diff --git a/scriptloader-support.ts b/scriptloader-support.ts new file mode 100644 index 0000000..e2f8911 --- /dev/null +++ b/scriptloader-support.ts @@ -0,0 +1 @@ +export * from "./src/scriptloader-support"; diff --git a/src/hooks/useScriptLoader.ts b/src/hooks/useScriptLoader.ts index 62eb6f2..ca1c7e2 100644 --- a/src/hooks/useScriptLoader.ts +++ b/src/hooks/useScriptLoader.ts @@ -1,19 +1,5 @@ -import { useCallback, useEffect } from "react"; - -import { - getFromWindowCache, - updateCachedScript, - CachedScript, - addScriptUpdater, - removeScriptUpdater, -} from "../scriptCache"; - -const getNewScript = (source: string): HTMLScriptElement => { - const newScript = document.createElement("script"); - newScript.async = true; - newScript.setAttribute("src", source); - return newScript; -}; +import { useCallback, useEffect, useRef } from "react"; +import { waitForScript } from "../scriptloader-support"; export interface ScriptLoaderConfiguration { onSuccess: () => void; @@ -25,105 +11,35 @@ export interface ScriptLoader { (config: ScriptLoaderConfiguration): void; } -interface CachedScriptUpdater extends ScriptLoader {} - -const useCachedScriptUpdater: CachedScriptUpdater = ({ - onSuccess, - onFailure, - source, -}) => { - const updater = useCallback( - ({ loading, failed, failureEvent }: CachedScript) => { - if (!loading && !failed) { - onSuccess(); - } - if (!loading && failed) { - onFailure(failureEvent); - } - }, - [onSuccess, onFailure] - ); - - useEffect(() => { - addScriptUpdater(source, updater); - return () => removeScriptUpdater(source, updater); - }, [updater, source]); - - useEffect(() => { - // run updater with already cached info - updater(getFromWindowCache(source)); - }, [source, updater]); -}; - const useScriptLoader: ScriptLoader = (config) => { - const { source } = config; - useCachedScriptUpdater(config); - - const setupListeners = useCallback( - (scriptRef: HTMLScriptElement): (() => void) => { - const removeListeners = () => { - scriptRef.removeEventListener("load", loadEvent); - scriptRef.removeEventListener("error", errorEvent); - }; - - const generateScriptEventListener = ( - getResultingCachedScript: (ev: Event) => Partial - ) => (ev: Event) => { - updateCachedScript(source, getResultingCachedScript(ev)); - removeListeners(); - }; - - const loadEvent = generateScriptEventListener(() => ({ - loading: false, - failed: false, - })); - - const errorEvent = generateScriptEventListener((err: ErrorEvent) => ({ - loading: false, - failed: true, - failureEvent: err, - })); - - scriptRef.addEventListener("load", loadEvent); - scriptRef.addEventListener("error", errorEvent); - - return removeListeners; + const { + source, + onSuccess, + onFailure = () => { + //noop }, - [source] + } = config; + const isMounted = useRef(true); + useEffect(() => () => (isMounted.current = false)); + const successFunc = useCallback(() => isMounted.current && onSuccess(), [ + onSuccess, + ]); + const errorFunc = useCallback( + (err: ErrorEvent) => isMounted.current && onFailure(err), + [onFailure] ); useEffect(() => { - let scriptRef = document.querySelector( - `script[src="${source}"]` - ); - const scriptExists = Boolean(scriptRef); - - if (scriptExists) { - const cachedScriptInfo = getFromWindowCache(source); - if (!cachedScriptInfo.scriptCreated) { - // if we did not create the script, assume it has loaded - updateCachedScript(source, { - loading: false, - failed: false, - }); - return; + const waitForSource = async () => { + try { + await waitForScript(source); + successFunc(); + } catch (err) { + errorFunc(err as ErrorEvent); } - - // if we are not loading, do nothing - if (!cachedScriptInfo.loading) return; - - // if we are loading and we did create the script, listen - return setupListeners(scriptRef); - } - - // if we did not create the script, create it - scriptRef = getNewScript(source); - updateCachedScript(source, { scriptCreated: true }); - const removeListeners = setupListeners(scriptRef); - document.body.appendChild(scriptRef); - - return removeListeners; - }, [source, setupListeners]); + }; + void waitForSource(); + }, [source, successFunc, errorFunc]); }; export default useScriptLoader; diff --git a/src/scriptloader-support/index.tsx b/src/scriptloader-support/index.tsx new file mode 100644 index 0000000..346aae5 --- /dev/null +++ b/src/scriptloader-support/index.tsx @@ -0,0 +1,95 @@ +import { + getFromWindowCache, + updateCachedScript, + CachedScript, + addScriptUpdater, + removeScriptUpdater, +} from "../scriptCache"; + +const getNewScript = (source: string): HTMLScriptElement => { + const newScript = document.createElement("script"); + newScript.async = true; + newScript.setAttribute("src", source); + return newScript; +}; + +const setupListeners = ( + scriptRef: HTMLScriptElement, + source: string +): (() => void) => { + const removeListeners = () => { + scriptRef.removeEventListener("load", loadEvent); + scriptRef.removeEventListener("error", errorEvent); + }; + + const generateScriptEventListener = ( + getResultingCachedScript: (ev: Event) => Partial + ) => (ev: Event) => { + updateCachedScript(source, getResultingCachedScript(ev)); + removeListeners(); + }; + + const loadEvent = generateScriptEventListener(() => ({ + loading: false, + failed: false, + })); + + const errorEvent = generateScriptEventListener((err: ErrorEvent) => ({ + loading: false, + failed: true, + failureEvent: err, + })); + + scriptRef.addEventListener("load", loadEvent); + scriptRef.addEventListener("error", errorEvent); + + return removeListeners; +}; + +export const waitForScript = (source: string): Promise => { + let scriptRef = document.querySelector( + `script[src="${source}"]` + ); + const scriptExists = Boolean(scriptRef); + const scriptPromise = new Promise((resolve, reject) => { + const updater = ({ loading, failed, failureEvent }: CachedScript) => { + if (!loading && !failed) { + resolve(); + removeScriptUpdater(source, updater); + } + if (!loading && failed) { + reject(failureEvent); + removeScriptUpdater(source, updater); + } + }; + addScriptUpdater(source, updater); + updater(getFromWindowCache(source)); + }); + + if (scriptExists) { + const cachedScriptInfo = getFromWindowCache(source); + if (!cachedScriptInfo.scriptCreated) { + // if we did not create the script, assume it has loaded + updateCachedScript(source, { + loading: false, + failed: false, + }); + return scriptPromise; + } + + // if we are not loading, do nothing + if (!cachedScriptInfo.loading) return scriptPromise; + + // if we are loading and we did create the script, listen + setupListeners(scriptRef, source); + return scriptPromise; + } + + // if we did not create the script, create it + scriptRef = getNewScript(source); + updateCachedScript(source, { scriptCreated: true }); + setupListeners(scriptRef, source); + document.body.appendChild(scriptRef); + + return scriptPromise; +}; diff --git a/tsconfig.json b/tsconfig.json index cbfdb6c..bfd2ca9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,10 @@ "target": "ES5", "typeRoots": ["node_modules/@types"] }, - "include": ["src", "./index.ts", "./jest.config.ts"] + "include": [ + "src", + "./index.ts", + "./scriptloader-support.ts", + "./jest.config.ts" + ] } From b3b1f89fd9aec7cbca14209f6fddee176a2ef3c5 Mon Sep 17 00:00:00 2001 From: Steve Matney Date: Thu, 7 Jan 2021 09:10:41 -0700 Subject: [PATCH 2/2] New: exporting scriptloader-support to be used outside of React --- .eslintrc.js | 2 +- package.json | 9 +++++++-- scriptloader-support.ts | 1 - scriptloader-support/index.d.ts | 1 + scriptloader-support/index.js | 2 ++ tsconfig.json | 12 +++++------- 6 files changed, 16 insertions(+), 11 deletions(-) delete mode 100644 scriptloader-support.ts create mode 100644 scriptloader-support/index.d.ts create mode 100644 scriptloader-support/index.js diff --git a/.eslintrc.js b/.eslintrc.js index 36dc00b..340036e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,7 +52,7 @@ module.exports = { }, }, { - files: ["./*.js"], + files: ["./*.js", "./scriptloader-support/*.js"], env: { node: true }, }, { diff --git a/package.json b/package.json index 4050d6b..800a79c 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,14 @@ "version": "1.3.0", "description": "A React Component for reacting to scripts loading.", "main": "dist/index.js", - "types": "dist/index.d.ts", + "exports": { + ".": "./dist/index.js", + "./scriptloader-support": "./dist/scriptloader-support/index.js" + }, "files": [ - "dist" + "dist", + "src", + "scriptloader-support" ], "scripts": { "test": "eslint --quiet . && tsc --noEmit --project ./tsconfig.json && jest", diff --git a/scriptloader-support.ts b/scriptloader-support.ts deleted file mode 100644 index e2f8911..0000000 --- a/scriptloader-support.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./src/scriptloader-support"; diff --git a/scriptloader-support/index.d.ts b/scriptloader-support/index.d.ts new file mode 100644 index 0000000..b016813 --- /dev/null +++ b/scriptloader-support/index.d.ts @@ -0,0 +1 @@ +export * from "../src/scriptloader-support"; diff --git a/scriptloader-support/index.js b/scriptloader-support/index.js new file mode 100644 index 0000000..b9e60fd --- /dev/null +++ b/scriptloader-support/index.js @@ -0,0 +1,2 @@ +const exports = require("../dist/src/scriptloader-support"); +module.exports = exports; diff --git a/tsconfig.json b/tsconfig.json index bfd2ca9..dc77707 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,12 +11,10 @@ "module": "commonjs", "moduleResolution": "node", "target": "ES5", - "typeRoots": ["node_modules/@types"] + "typeRoots": ["node_modules/@types"], + "paths": { + "scriptloader-support/*": ["./src/scriptloader-support/*"] + } }, - "include": [ - "src", - "./index.ts", - "./scriptloader-support.ts", - "./jest.config.ts" - ] + "include": ["src", "./index.ts", "./jest.config.ts"] }