Skip to content

Commit 301b78e

Browse files
author
Philipp Molitor
committed
rework the useScript hook
1 parent 1dd530c commit 301b78e

File tree

2 files changed

+78
-37
lines changed

2 files changed

+78
-37
lines changed

src/hooks/__tests__/useScript.test.tsx

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ describe('useScript()', () => {
4444
expect(state).toStrictEqual<ScriptStatus>('loading');
4545
});
4646

47-
// TODO
4847
it('is in "active" state when the "onload" event was triggered', async () => {
4948
const { result } = renderHook(() => useScript(scriptUrl));
5049

@@ -61,6 +60,22 @@ describe('useScript()', () => {
6160
expect(result.current[0]).toStrictEqual<ScriptStatus>('active');
6261
});
6362

63+
it('is in "error" state when the "onerror" event was triggered', async () => {
64+
const { result } = renderHook(() => useScript(scriptUrl));
65+
66+
expect(result.current[0]).toStrictEqual<ScriptStatus>('loading');
67+
68+
const element = document.querySelector(
69+
`script[src="${scriptUrl}"]`
70+
) as HTMLScriptElement;
71+
72+
act(() => {
73+
fireEvent.error(element);
74+
});
75+
76+
expect(result.current[0]).toStrictEqual<ScriptStatus>('error');
77+
});
78+
6479
it('is in "unloaded" state when the source is removed', async () => {
6580
const { result } = renderHook(() => useScript(scriptUrl));
6681

@@ -73,7 +88,7 @@ describe('useScript()', () => {
7388
expect(result.current[0]).toStrictEqual<ScriptStatus>('unloaded');
7489
});
7590

76-
it('creates a new script and discards the old one when changing the source URL', async () => {
91+
it('reuses the old script tag when changing the source URL', async () => {
7792
const newUrl = 'https://test.com/loader.js';
7893
const { result } = renderHook(() => useScript(scriptUrl));
7994

@@ -88,12 +103,11 @@ describe('useScript()', () => {
88103
result.current[1](newUrl);
89104
});
90105

91-
expect(result.current[0]).toStrictEqual<ScriptStatus>('loading');
92-
expect(element).not.toBeInTheDocument();
93-
94106
const newElement = document.querySelector(
95107
`script[src="${newUrl}"]`
96108
) as HTMLScriptElement;
109+
110+
expect(element).not.toBeInTheDocument();
97111
expect(newElement).toBeInTheDocument();
98112
});
99113

@@ -114,4 +128,56 @@ describe('useScript()', () => {
114128
expect(result.current[0]).toStrictEqual<ScriptStatus>('loading');
115129
expect(element).toBeInTheDocument();
116130
});
131+
132+
it('handles an existing script', async () => {
133+
const script = document.createElement('script');
134+
script.src = scriptUrl;
135+
document.body.appendChild(script);
136+
137+
const { result } = renderHook(() => useScript(scriptUrl));
138+
139+
const element = document.querySelector(
140+
`script[src="${scriptUrl}"]`
141+
) as HTMLScriptElement;
142+
143+
expect(result.current[0]).toStrictEqual<ScriptStatus>('loading');
144+
expect(element.src).toStrictEqual(scriptUrl);
145+
146+
act(() => {
147+
result.current[1](scriptUrl);
148+
});
149+
150+
expect(result.current[0]).toStrictEqual<ScriptStatus>('loading');
151+
expect(element).toBeInTheDocument();
152+
});
153+
154+
it('removes the <script> tag when setting the source to undefined', async () => {
155+
const { result } = renderHook(() => useScript(scriptUrl));
156+
157+
const element = document.querySelector(
158+
`script[src="${scriptUrl}"]`
159+
) as HTMLScriptElement;
160+
161+
expect(element).toBeInTheDocument();
162+
163+
act(() => {
164+
result.current[1]();
165+
});
166+
167+
expect(element).not.toBeInTheDocument();
168+
});
169+
170+
it('removes the <script> tag on unmount', async () => {
171+
const { unmount } = renderHook(() => useScript(scriptUrl));
172+
173+
const element = document.querySelector(
174+
`script[src="${scriptUrl}"]`
175+
) as HTMLScriptElement;
176+
177+
expect(element).toBeInTheDocument();
178+
179+
unmount();
180+
181+
expect(element).not.toBeInTheDocument();
182+
});
117183
});

src/hooks/useScript.ts

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,6 @@ export type UseScriptValue = [
1111

1212
export type ScriptStatus = 'unloaded' | 'loading' | 'active' | 'error';
1313

14-
function setScriptDataAttribute(
15-
script: HTMLScriptElement | undefined,
16-
status: ScriptStatus
17-
) {
18-
script?.setAttribute('data-state', status);
19-
}
20-
2114
export const useScript = (src?: string): UseScriptValue => {
2215
const [source, setSource] = useState<string | undefined>(src);
2316
const [scriptState, setScriptState] = useState<ScriptStatus>(
@@ -26,55 +19,37 @@ export const useScript = (src?: string): UseScriptValue => {
2619

2720
// on state change
2821
useEffect(() => {
29-
// get a reference to the script element
30-
let script = document.querySelector(`script[src="${src}"]`) as
22+
let script = document.querySelector(`script[src="${source}"]`) as
3123
| HTMLScriptElement
3224
| undefined;
3325

34-
// unload the script
3526
if (!source) {
3627
setScriptState('unloaded');
37-
if (script) script.remove();
3828
return;
3929
}
4030

4131
// if script is not in DOM yet, add it
42-
if (!script) {
32+
if (source && !script) {
4333
script = document.createElement('script');
4434
script.src = source;
4535
script.async = true;
46-
setScriptDataAttribute(script, 'loading');
4736

48-
// callback on script load/error to update the script data attribute
49-
const setAttributeCallback = (event: Event) => {
50-
setScriptDataAttribute(
51-
script,
37+
// callback on script load/error to update the hooks state
38+
const setStateCallback = (event: Event) => {
39+
setScriptState(
5240
(event.type as keyof HTMLElementEventMap) === 'load'
5341
? 'active'
5442
: 'error'
5543
);
5644
};
5745

58-
// attach callback to event handlers
59-
script.addEventListener('load', setAttributeCallback);
60-
script.addEventListener('error', setAttributeCallback);
46+
script.addEventListener('load', setStateCallback);
47+
script.addEventListener('error', setStateCallback);
6148

6249
// attach the script to the DOM
6350
document.body.appendChild(script);
6451
}
6552

66-
// callback on script load/error to update the hooks state
67-
const setStateCallback = (event: Event) => {
68-
setScriptState(
69-
(event.type as keyof HTMLElementEventMap) === 'load'
70-
? 'active'
71-
: 'error'
72-
);
73-
};
74-
75-
script.addEventListener('load', setStateCallback);
76-
script.addEventListener('error', setStateCallback);
77-
7853
// eslint-disable-next-line consistent-return
7954
return () => {
8055
if (script) script.remove();

0 commit comments

Comments
 (0)