Skip to content

Commit 94a4035

Browse files
committed
refactor: rewrites most of the logic
BREAKING CHANGE: changes the api in order to be more versatile
1 parent ecfd791 commit 94a4035

File tree

4 files changed

+246
-197
lines changed

4 files changed

+246
-197
lines changed

README.md

Lines changed: 75 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,27 @@ npm install --save react-use-scripts
2727

2828
## Usage
2929

30-
- Use script tags in your **JSX**
30+
- react-use-scripts will return a default export _useScript_ and a named export _{ ScriptLoader }_
31+
- Use ScriptLoader as an element in your **JSX** and add optional children and/or fallback rendering
3132

3233
```tsx
3334
import * as React from 'react';
34-
import { useScript } from 'react-use-scripts';
35+
import { ScriptLoader } from 'react-use-scripts';
3536

3637
const App = () => {
37-
const { ScriptLoader } = useScript();
38-
3938
return (
40-
<div>
41-
<ScriptLoader
42-
id="custom-script"
43-
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
44-
delayMs={0}
45-
onCreate={() => console.log('created!')}
46-
type="text/javascript"
47-
/>
48-
</div>
39+
<ScriptLoader
40+
id="custom-script-id"
41+
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
42+
delay={500}
43+
onReady={() => console.log('ready!')}
44+
onError={(error) => console.log('an error has happened!', error)}
45+
fallback={(error) => (
46+
<span>This has errored! {JSON.stringify(error)}</span>
47+
)}
48+
>
49+
<span>Script has loaded succesfully!
50+
</ScriptLoader>
4951
);
5052
};
5153
```
@@ -54,22 +56,28 @@ const App = () => {
5456

5557
```tsx
5658
import * as React from 'react';
57-
import { useScript } from 'react-use-scripts';
59+
import useScript from 'react-use-scripts';
5860

5961
const App = () => {
60-
const { appendScript } = useScript();
61-
62-
React.useEffect(() => {
63-
appendScript({
64-
id: 'script-append',
65-
scriptText: "console.log('my script has been called')",
66-
optionalCallback: console.log('optional callback'),
67-
});
68-
}, [appendScript]);
62+
const [startTrigger, setStartTrigger] = React.useState(false);
63+
const { ready, error } = useScript({
64+
src: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js',
65+
onReady: () => console.log('ready!'),
66+
onError: (error) => console.log('an error has happened!', error),
67+
startTrigger,
68+
});
69+
70+
const handleAppendScriptClick = () => {
71+
setStartTrigger(true);
72+
};
6973

7074
return (
7175
<div>
72-
<h1>Script appended to the head programmatically!</h1>
76+
<button onClick={handleAppendScriptClick}>
77+
Click to start appending
78+
</button>
79+
{ready && <h1>Script appended to the head programmatically!</h1>}
80+
{error && <h1>Script has errored! {JSON.stringify(error)}</h1>}
7381
</div>
7482
);
7583
};
@@ -79,59 +87,54 @@ const App = () => {
7987

8088
## Documentation
8189

82-
- `useScript()` returns:
83-
84-
- ScriptLoader as component
85-
- Props:
90+
1. `ScriptLoader`: **all props** are optional but without either _src_ or _innerText_ this will return `null`;
8691

87-
```tsx
88-
type ScriptLoader = {
89-
onCreate?: (() => null) | undefined; // runs after script tag rendering
90-
onLoad?: (() => null) | undefined; // runs on script load
91-
onError?: ((e: any) => never) | undefined; // runs on script error
92-
delayMs?: number | undefined; // run with delayed start
93-
htmlPart?: string | undefined; // choose where to append, HEAD or BODY
94-
src: string; // script file source path
95-
otherProps?: Record<string, unknown> | undefined; // html script tag properties
96-
};
97-
```
92+
```tsx
93+
interface IScriptLoaderProps {
94+
src?: string;
95+
innerText?: string;
96+
onReady?: () => void;
97+
onError?: (error: string | Event) => void;
98+
otherProps?: THTMLScriptElementProps;
99+
startTrigger?: boolean;
100+
id?: string;
101+
appendTo?: string;
102+
delay?: number;
103+
children?:
104+
| JSX.Element
105+
| JSX.Element[]
106+
| string
107+
| string[]
108+
| number
109+
| number[];
110+
fallback?: (error: string | Event) => JSX.Element;
111+
}
112+
```
98113

99-
- Default Props:
114+
2. useScript
100115

101-
```tsx
102-
src: undefined;
103-
onCreate = () => null;
104-
onLoad = () => null;
105-
onError = (e) => {
106-
throw new URIError(`The script ${e.target.src} is not accessible`);
107-
};
108-
delayMs = 0;
109-
htmlPart = 'head';
110-
otherProps: undefined;
111-
```
112-
113-
- appendScript()
114-
- Props:
115-
116-
```tsx
117-
type AppendScript = {
118-
id: string; // script id
119-
scriptText: string; // script code as string
120-
optionalCallback?: (() => null) | undefined; // optional callback function after running
121-
htmlPart: string; // choose where to append, HEAD or BODY
122-
otherProps?: Record<string, unknown> | undefined; // html script tag properties
123-
};
124-
```
116+
```tsx
117+
interface IScriptProps {
118+
src?: string;
119+
innerText?: string;
120+
onReady?: () => void;
121+
onError?: (error: string | Event) => void;
122+
otherProps?: THTMLScriptElementProps;
123+
startTrigger?: boolean;
124+
id?: string;
125+
appendTo?: string;
126+
delay?: number;
127+
}
128+
```
125129

126-
- Default Props:
130+
- Default Props:
127131

128-
```tsx
129-
id: undefined;
130-
scriptText: undefined;
131-
optionalCallback = () => null;
132-
htmlPart = 'head';
133-
otherProps = {};
134-
```
132+
```tsx
133+
startTrigger = true,
134+
id = `react-use-script-${new Date().toISOString()}`,
135+
appendTo = 'head',
136+
delay = 0,
137+
```
135138

136139
---
137140

rollup.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { terser } from 'rollup-plugin-terser';
77
import pkg from './package.json';
88

99
export default {
10-
input: 'src/index.tsx',
10+
input: 'src/index.ts',
1111
output: [
1212
{
1313
file: pkg.main,

src/index.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import * as React from 'react';
2+
3+
type THTMLScriptElementProps = Record<string, keyof HTMLScriptElement>;
4+
5+
export interface IUseScript {
6+
ready: boolean;
7+
error: null | Event | string;
8+
}
9+
10+
export interface IScriptProps {
11+
src?: string;
12+
innerText?: string;
13+
onReady?: () => void;
14+
onError?: (error: string | Event) => void;
15+
otherProps?: THTMLScriptElementProps;
16+
startTrigger?: boolean;
17+
id?: string;
18+
appendTo?: string;
19+
delay?: number;
20+
}
21+
22+
export interface IScriptLoaderProps extends IScriptProps {
23+
children?:
24+
| JSX.Element
25+
| JSX.Element[]
26+
| string
27+
| string[]
28+
| number
29+
| number[];
30+
fallback?: (error: string | Event) => JSX.Element;
31+
}
32+
33+
const handleScriptAttributes = (
34+
script: HTMLScriptElement,
35+
otherProps: THTMLScriptElementProps
36+
) => {
37+
for (const [attr, value] of Object.entries(otherProps)) {
38+
script.setAttribute(attr, value as string);
39+
}
40+
};
41+
42+
export default function useScript({
43+
src,
44+
innerText,
45+
onReady,
46+
onError,
47+
otherProps,
48+
startTrigger = true,
49+
id = `react-use-script-${Math.random()}`,
50+
appendTo = 'head',
51+
delay = 0,
52+
}: IScriptProps): IUseScript {
53+
const isLoading = React.useRef(false);
54+
const [state, setState] = React.useState<IUseScript>({
55+
ready: false,
56+
error: null,
57+
});
58+
const handleOnLoad = React.useCallback(() => {
59+
setState(() => ({ ready: true, error: null }));
60+
onReady?.();
61+
}, [onReady]);
62+
const handleOnError = React.useCallback(
63+
(error) => {
64+
setState(() => ({ ready: false, error }));
65+
onError?.(error);
66+
},
67+
[onError]
68+
);
69+
const canRunEffect =
70+
(typeof src === 'string' && src?.length > 0) ||
71+
(typeof innerText === 'string' && innerText?.length > 0);
72+
73+
React.useEffect(() => {
74+
if (canRunEffect && startTrigger && !isLoading.current) {
75+
setTimeout(() => {
76+
try {
77+
const script = global.document.createElement('script');
78+
79+
if (innerText && !src) {
80+
script.innerText = innerText.toString();
81+
}
82+
83+
if (src && !innerText) {
84+
script.src = src.toString();
85+
}
86+
87+
script.id = id;
88+
89+
if (otherProps) {
90+
handleScriptAttributes(script, otherProps);
91+
}
92+
93+
script.onload = () => handleOnLoad();
94+
95+
script.onerror = handleOnError;
96+
97+
global.document[appendTo].appendChild(script);
98+
99+
isLoading.current = true;
100+
101+
if (innerText && !src) {
102+
handleOnLoad();
103+
}
104+
} catch (error) {
105+
handleOnError(error);
106+
}
107+
}, delay);
108+
}
109+
}, [
110+
onReady,
111+
onError,
112+
otherProps,
113+
startTrigger,
114+
id,
115+
appendTo,
116+
delay,
117+
handleOnLoad,
118+
handleOnError,
119+
canRunEffect,
120+
innerText,
121+
src,
122+
]);
123+
124+
return state;
125+
}
126+
127+
export const ScriptLoader = ({
128+
children,
129+
fallback,
130+
src,
131+
innerText,
132+
onReady,
133+
onError,
134+
otherProps,
135+
startTrigger = true,
136+
id = `react-use-script-${new Date().toISOString()}`,
137+
appendTo = 'head',
138+
delay = 0,
139+
}: IScriptLoaderProps):
140+
| string
141+
| number
142+
| JSX.Element
143+
| JSX.Element[]
144+
| string[]
145+
| number[]
146+
| null => {
147+
const { ready, error } = useScript({
148+
src,
149+
innerText,
150+
onReady,
151+
onError,
152+
startTrigger,
153+
id,
154+
appendTo,
155+
delay,
156+
otherProps,
157+
});
158+
159+
console.log('state', { ready, error });
160+
161+
if (ready && children) {
162+
return children;
163+
}
164+
165+
if (error && fallback) {
166+
return fallback(error);
167+
}
168+
169+
return null;
170+
};

0 commit comments

Comments
 (0)