Skip to content

Commit 80aedf8

Browse files
authored
Merge pull request #3 from WTW-IM/scriptloader-helpers
Update: utilizing promises for loading scripts
2 parents 973ffc3 + b3b1f89 commit 80aedf8

File tree

7 files changed

+136
-114
lines changed

7 files changed

+136
-114
lines changed

.eslintrc.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ module.exports = {
3131
extends: baseExtends,
3232
ignorePatterns: ["dist/**/*"],
3333
env: { es6: true },
34-
parserOptions: { ecmaVersion: 2017 },
34+
parserOptions: { ecmaVersion: 2021, sourceType: "module" },
3535
overrides: [
3636
{
3737
files: ["src/**/*", "./index.ts", "./jest.config.ts"],
@@ -52,7 +52,7 @@ module.exports = {
5252
},
5353
},
5454
{
55-
files: ["./*.js"],
55+
files: ["./*.js", "./scriptloader-support/*.js"],
5656
env: { node: true },
5757
},
5858
{

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
"version": "1.3.0",
44
"description": "A React Component for reacting to scripts loading.",
55
"main": "dist/index.js",
6-
"types": "dist/index.d.ts",
6+
"exports": {
7+
".": "./dist/index.js",
8+
"./scriptloader-support": "./dist/scriptloader-support/index.js"
9+
},
710
"files": [
8-
"dist"
11+
"dist",
12+
"src",
13+
"scriptloader-support"
914
],
1015
"scripts": {
1116
"test": "eslint --quiet . && tsc --noEmit --project ./tsconfig.json && jest",

scriptloader-support/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "../src/scriptloader-support";

scriptloader-support/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const exports = require("../dist/src/scriptloader-support");
2+
module.exports = exports;

src/hooks/useScriptLoader.ts

Lines changed: 25 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,5 @@
1-
import { useCallback, useEffect } from "react";
2-
3-
import {
4-
getFromWindowCache,
5-
updateCachedScript,
6-
CachedScript,
7-
addScriptUpdater,
8-
removeScriptUpdater,
9-
} from "../scriptCache";
10-
11-
const getNewScript = (source: string): HTMLScriptElement => {
12-
const newScript = document.createElement("script");
13-
newScript.async = true;
14-
newScript.setAttribute("src", source);
15-
return newScript;
16-
};
1+
import { useCallback, useEffect, useRef } from "react";
2+
import { waitForScript } from "../scriptloader-support";
173

184
export interface ScriptLoaderConfiguration {
195
onSuccess: () => void;
@@ -25,105 +11,35 @@ export interface ScriptLoader {
2511
(config: ScriptLoaderConfiguration): void;
2612
}
2713

28-
interface CachedScriptUpdater extends ScriptLoader {}
29-
30-
const useCachedScriptUpdater: CachedScriptUpdater = ({
31-
onSuccess,
32-
onFailure,
33-
source,
34-
}) => {
35-
const updater = useCallback(
36-
({ loading, failed, failureEvent }: CachedScript) => {
37-
if (!loading && !failed) {
38-
onSuccess();
39-
}
40-
if (!loading && failed) {
41-
onFailure(failureEvent);
42-
}
43-
},
44-
[onSuccess, onFailure]
45-
);
46-
47-
useEffect(() => {
48-
addScriptUpdater(source, updater);
49-
return () => removeScriptUpdater(source, updater);
50-
}, [updater, source]);
51-
52-
useEffect(() => {
53-
// run updater with already cached info
54-
updater(getFromWindowCache(source));
55-
}, [source, updater]);
56-
};
57-
5814
const useScriptLoader: ScriptLoader = (config) => {
59-
const { source } = config;
60-
useCachedScriptUpdater(config);
61-
62-
const setupListeners = useCallback(
63-
(scriptRef: HTMLScriptElement): (() => void) => {
64-
const removeListeners = () => {
65-
scriptRef.removeEventListener("load", loadEvent);
66-
scriptRef.removeEventListener("error", errorEvent);
67-
};
68-
69-
const generateScriptEventListener = (
70-
getResultingCachedScript: (ev: Event) => Partial<CachedScript>
71-
) => (ev: Event) => {
72-
updateCachedScript(source, getResultingCachedScript(ev));
73-
removeListeners();
74-
};
75-
76-
const loadEvent = generateScriptEventListener(() => ({
77-
loading: false,
78-
failed: false,
79-
}));
80-
81-
const errorEvent = generateScriptEventListener((err: ErrorEvent) => ({
82-
loading: false,
83-
failed: true,
84-
failureEvent: err,
85-
}));
86-
87-
scriptRef.addEventListener("load", loadEvent);
88-
scriptRef.addEventListener("error", errorEvent);
89-
90-
return removeListeners;
15+
const {
16+
source,
17+
onSuccess,
18+
onFailure = () => {
19+
//noop
9120
},
92-
[source]
21+
} = config;
22+
const isMounted = useRef(true);
23+
useEffect(() => () => (isMounted.current = false));
24+
const successFunc = useCallback(() => isMounted.current && onSuccess(), [
25+
onSuccess,
26+
]);
27+
const errorFunc = useCallback(
28+
(err: ErrorEvent) => isMounted.current && onFailure(err),
29+
[onFailure]
9330
);
9431

9532
useEffect(() => {
96-
let scriptRef = document.querySelector<HTMLScriptElement>(
97-
`script[src="${source}"]`
98-
);
99-
const scriptExists = Boolean(scriptRef);
100-
101-
if (scriptExists) {
102-
const cachedScriptInfo = getFromWindowCache(source);
103-
if (!cachedScriptInfo.scriptCreated) {
104-
// if we did not create the script, assume it has loaded
105-
updateCachedScript(source, {
106-
loading: false,
107-
failed: false,
108-
});
109-
return;
33+
const waitForSource = async () => {
34+
try {
35+
await waitForScript(source);
36+
successFunc();
37+
} catch (err) {
38+
errorFunc(err as ErrorEvent);
11039
}
111-
112-
// if we are not loading, do nothing
113-
if (!cachedScriptInfo.loading) return;
114-
115-
// if we are loading and we did create the script, listen
116-
return setupListeners(scriptRef);
117-
}
118-
119-
// if we did not create the script, create it
120-
scriptRef = getNewScript(source);
121-
updateCachedScript(source, { scriptCreated: true });
122-
const removeListeners = setupListeners(scriptRef);
123-
document.body.appendChild(scriptRef);
124-
125-
return removeListeners;
126-
}, [source, setupListeners]);
40+
};
41+
void waitForSource();
42+
}, [source, successFunc, errorFunc]);
12743
};
12844

12945
export default useScriptLoader;

src/scriptloader-support/index.tsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
getFromWindowCache,
3+
updateCachedScript,
4+
CachedScript,
5+
addScriptUpdater,
6+
removeScriptUpdater,
7+
} from "../scriptCache";
8+
9+
const getNewScript = (source: string): HTMLScriptElement => {
10+
const newScript = document.createElement("script");
11+
newScript.async = true;
12+
newScript.setAttribute("src", source);
13+
return newScript;
14+
};
15+
16+
const setupListeners = (
17+
scriptRef: HTMLScriptElement,
18+
source: string
19+
): (() => void) => {
20+
const removeListeners = () => {
21+
scriptRef.removeEventListener("load", loadEvent);
22+
scriptRef.removeEventListener("error", errorEvent);
23+
};
24+
25+
const generateScriptEventListener = (
26+
getResultingCachedScript: (ev: Event) => Partial<CachedScript>
27+
) => (ev: Event) => {
28+
updateCachedScript(source, getResultingCachedScript(ev));
29+
removeListeners();
30+
};
31+
32+
const loadEvent = generateScriptEventListener(() => ({
33+
loading: false,
34+
failed: false,
35+
}));
36+
37+
const errorEvent = generateScriptEventListener((err: ErrorEvent) => ({
38+
loading: false,
39+
failed: true,
40+
failureEvent: err,
41+
}));
42+
43+
scriptRef.addEventListener("load", loadEvent);
44+
scriptRef.addEventListener("error", errorEvent);
45+
46+
return removeListeners;
47+
};
48+
49+
export const waitForScript = (source: string): Promise<void> => {
50+
let scriptRef = document.querySelector<HTMLScriptElement>(
51+
`script[src="${source}"]`
52+
);
53+
const scriptExists = Boolean(scriptRef);
54+
const scriptPromise = new Promise<void>((resolve, reject) => {
55+
const updater = ({ loading, failed, failureEvent }: CachedScript) => {
56+
if (!loading && !failed) {
57+
resolve();
58+
removeScriptUpdater(source, updater);
59+
}
60+
if (!loading && failed) {
61+
reject(failureEvent);
62+
removeScriptUpdater(source, updater);
63+
}
64+
};
65+
addScriptUpdater(source, updater);
66+
updater(getFromWindowCache(source));
67+
});
68+
69+
if (scriptExists) {
70+
const cachedScriptInfo = getFromWindowCache(source);
71+
if (!cachedScriptInfo.scriptCreated) {
72+
// if we did not create the script, assume it has loaded
73+
updateCachedScript(source, {
74+
loading: false,
75+
failed: false,
76+
});
77+
return scriptPromise;
78+
}
79+
80+
// if we are not loading, do nothing
81+
if (!cachedScriptInfo.loading) return scriptPromise;
82+
83+
// if we are loading and we did create the script, listen
84+
setupListeners(scriptRef, source);
85+
return scriptPromise;
86+
}
87+
88+
// if we did not create the script, create it
89+
scriptRef = getNewScript(source);
90+
updateCachedScript(source, { scriptCreated: true });
91+
setupListeners(scriptRef, source);
92+
document.body.appendChild(scriptRef);
93+
94+
return scriptPromise;
95+
};

tsconfig.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
"module": "commonjs",
1212
"moduleResolution": "node",
1313
"target": "ES5",
14-
"typeRoots": ["node_modules/@types"]
14+
"typeRoots": ["node_modules/@types"],
15+
"paths": {
16+
"scriptloader-support/*": ["./src/scriptloader-support/*"]
17+
}
1518
},
1619
"include": ["src", "./index.ts", "./jest.config.ts"]
1720
}

0 commit comments

Comments
 (0)