Skip to content

Commit 5454a03

Browse files
committed
fix(ssr): copy SSR fixes from react-render-props-link-loader
1 parent 3965269 commit 5454a03

File tree

3 files changed

+122
-114
lines changed

3 files changed

+122
-114
lines changed

src/index.js

Lines changed: 59 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,30 @@ export type Props = {
1818
children?: ?(state: State) => ?React.Node,
1919
}
2020

21+
export type InnerProps = Props & {
22+
scriptsRegistry?: ?ScriptsRegistry,
23+
}
24+
25+
export class ScriptsRegistry {
26+
scripts: Array<{
27+
src: string,
28+
}> = []
29+
results: { [src: string]: { error: ?Error } } = {}
30+
promises: { [src: string]: Promise<any> } = {}
31+
32+
scriptTags(): React.Node {
33+
return this.scripts.map(props => <script key={props.src} {...props} />)
34+
}
35+
}
36+
2137
export const ScriptsRegistryContext: React.Context<?ScriptsRegistry> = React.createContext(
2238
null
2339
)
2440

25-
export default class ScriptLoader extends React.PureComponent<Props, State> {
41+
class ScriptLoader extends React.PureComponent<InnerProps, State> {
42+
mounted: boolean = false
43+
promise: Promise<void> = loadScript(this.props)
2644
state = getState(this.props)
27-
promise: ?Promise<void>
2845

2946
static propTypes = {
3047
src: PropTypes.string.isRequired,
@@ -33,91 +50,56 @@ export default class ScriptLoader extends React.PureComponent<Props, State> {
3350
children: PropTypes.func,
3451
}
3552

36-
load() {
37-
const { props } = this
38-
const {
39-
onLoad,
40-
onError,
41-
children, // eslint-disable-line no-unused-vars
42-
...loadProps
43-
} = props
44-
const promise = loadScript(loadProps)
45-
if (this.promise !== promise) {
46-
this.promise = promise
47-
this.setState(getState(props))
48-
promise.then(
49-
() => {
50-
if (this.promise !== promise) return
51-
if (onLoad) onLoad()
52-
this.setState(getState(props))
53-
},
54-
(error: Error) => {
55-
if (this.promise !== promise) return
56-
if (onError) onError(error)
57-
this.setState(getState(props))
58-
}
59-
)
60-
}
53+
componentDidMount() {
54+
this.mounted = true
55+
this.listenTo(this.promise)
6156
}
6257

63-
componentDidMount() {
64-
this.load()
58+
componentWillUnmount() {
59+
this.mounted = false
6560
}
6661

6762
componentDidUpdate() {
68-
this.load()
63+
const promise = loadScript(this.props)
64+
if (this.promise !== promise) {
65+
this.setState(getState(this.props))
66+
this.promise = promise
67+
this.listenTo(promise)
68+
}
6969
}
7070

71-
componentWillUnmount() {
72-
this.promise = null
71+
listenTo(promise: Promise<any>) {
72+
const { props } = this
73+
const { onLoad, onError } = props
74+
promise.then(
75+
() => {
76+
if (!this.mounted || this.promise !== promise) return
77+
if (onLoad) onLoad()
78+
this.setState(getState(props))
79+
},
80+
(error: Error) => {
81+
if (!this.mounted || this.promise !== promise) return
82+
if (onError) onError(error)
83+
this.setState(getState(props))
84+
}
85+
)
7386
}
7487

7588
render(): React.Node {
76-
const {
77-
children,
78-
/* eslint-disable no-unused-vars */
79-
onLoad,
80-
onError,
81-
/* eslint-enable no-unsued-vars */
82-
...props
83-
} = this.props
84-
return (
85-
<ScriptsRegistryContext.Consumer>
86-
{(context: ?ScriptsRegistry) => {
87-
if (context) {
88-
context.scripts.push(props)
89-
if (!children) return <React.Fragment />
90-
const result = children({
91-
loading: true,
92-
loaded: false,
93-
error: null,
94-
promise: new Promise(() => {}),
95-
})
96-
return result == null ? null : result
97-
}
98-
if (children) {
99-
const result = children({ ...this.state })
100-
return result == null ? null : result
101-
}
102-
return null
103-
}}
104-
</ScriptsRegistryContext.Consumer>
105-
)
89+
const { children } = this.props
90+
if (children) {
91+
const result = children({ ...this.state })
92+
return result == null ? null : result
93+
}
94+
return null
10695
}
10796
}
10897

109-
export class ScriptsRegistry {
110-
scripts: Array<{
111-
src: string,
112-
}> = []
113-
114-
scriptTags(): React.Node {
115-
return (
116-
<React.Fragment>
117-
{this.scripts.map((props, index) => (
118-
<script key={index} {...props} />
119-
))}
120-
</React.Fragment>
121-
)
122-
}
123-
}
98+
const ConnectedScriptsLoader = (props: Props) => (
99+
<ScriptsRegistryContext.Consumer>
100+
{scriptsRegistry => (
101+
<ScriptLoader {...props} scriptsRegistry={scriptsRegistry} />
102+
)}
103+
</ScriptsRegistryContext.Consumer>
104+
)
105+
export default ConnectedScriptsLoader

src/loadScript.js

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,77 @@
11
// @flow
2-
32
/* eslint-env browser */
3+
import { type InnerProps } from './index'
44

5-
type Props = {
6-
src: string,
7-
}
8-
9-
const loadScript = ({ src, ...props }: Props): Promise<void> =>
10-
new Promise((resolve: () => void, reject: (error?: Error) => void) => {
11-
if (typeof document === 'undefined') {
12-
reject(new Error('server-side rendering is not supported'))
5+
const loadScript = async ({
6+
scriptsRegistry,
7+
onLoad,
8+
onError,
9+
children,
10+
...props
11+
}: InnerProps): Promise<void> => {
12+
const { src } = props
13+
if (scriptsRegistry) {
14+
scriptsRegistry.results[src] = { error: undefined }
15+
scriptsRegistry.scripts.push(props)
16+
return
17+
}
18+
if (typeof document === 'undefined') {
19+
throw new Error(
20+
'you must pass a scriptsRegistry if calling on the server side'
21+
)
22+
}
23+
if (typeof document.querySelector === 'function') {
24+
if (document.querySelector(`script[src="${src}"]`)) {
25+
results[src] = { error: undefined }
1326
return
1427
}
15-
if (typeof document.querySelector === 'function') {
16-
if (document.querySelector(`script[src="${src}"]`)) {
17-
resolve()
18-
return
19-
}
20-
}
28+
}
29+
return new Promise((resolve: () => void, reject: (error?: Error) => void) => {
2130
const script = document.createElement('script')
2231
script.src = src
2332
Object.keys(props).forEach(key => script.setAttribute(key, props[key]))
2433
script.onload = resolve
2534
script.onerror = reject
2635
if (document.body) document.body.appendChild(script)
2736
})
37+
}
2838

2939
const results: { [src: string]: { error: ?Error } } = {}
3040
const promises: { [src: string]: Promise<any> } = {}
3141

32-
export default (props: Props): Promise<any> =>
33-
promises[props.src] ||
34-
(promises[props.src] = loadScript(props).then(
35-
() => (results[props.src] = { error: null }),
36-
(error: any = new Error(`failed to load ${props.src}`)) => {
37-
results[props.src] = { error }
38-
throw error
39-
}
40-
))
42+
export default (props: InnerProps): Promise<any> => {
43+
const { scriptsRegistry } = props
44+
const _promises = scriptsRegistry ? scriptsRegistry.promises : promises
45+
const _results = scriptsRegistry ? scriptsRegistry.results : results
46+
return (
47+
_promises[props.src] ||
48+
(_promises[props.src] = loadScript(props).then(
49+
() => (_results[props.src] = { error: null }),
50+
(error: any = new Error(`failed to load ${props.src}`)) => {
51+
_results[props.src] = { error }
52+
throw error
53+
}
54+
))
55+
)
56+
}
4157

4258
export function getState({
4359
src,
44-
}: Props): {
60+
scriptsRegistry,
61+
}: InnerProps): {
4562
loading: boolean,
4663
loaded: boolean,
4764
error: ?Error,
4865
promise: ?Promise<any>,
4966
} {
50-
const result = results[src]
67+
const result = scriptsRegistry ? scriptsRegistry.results[src] : results[src]
68+
const promise = scriptsRegistry
69+
? scriptsRegistry.promises[src]
70+
: promises[src]
5171
return {
5272
loading: result == null,
5373
loaded: result ? !result.error : false,
5474
error: result && result.error,
55-
promise: promises[src],
75+
promise,
5676
}
5777
}

test/index.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ describe('ScriptLoader', () => {
9191
)
9292
expect(comp.text()).to.equal('hello')
9393
expect(render.lastCall.lastArg).to.containSubset({
94-
loading: true,
95-
loaded: false,
94+
loading: false,
95+
loaded: true,
9696
error: undefined,
9797
})
9898
const script = document.getElementById('scriptId')
@@ -252,20 +252,26 @@ describe(`SSR`, function() {
252252
this.timeout(10000)
253253
const render = sinon.spy(() => 'hello')
254254
const registry = new ScriptsRegistry()
255-
mount(
255+
const comp = mount(
256256
<ScriptsRegistryContext.Provider value={registry}>
257-
<ScriptLoader src="foo" id="scriptId">
257+
<ScriptLoader src="SSR" id="scriptId">
258258
{render}
259259
</ScriptLoader>
260260
</ScriptsRegistryContext.Provider>
261261
)
262-
const comp = mount(registry.scriptTags())
263-
264262
expect(render.lastCall.lastArg).to.containSubset({
265-
loading: true,
266-
loaded: false,
267-
error: null,
263+
loading: false,
264+
loaded: true,
265+
error: undefined,
268266
})
269-
expect(comp.find('script').prop('src')).to.equal('foo')
267+
expect(comp.text()).to.equal('hello')
268+
269+
const head = mount(
270+
<head>
271+
<meta key={0} />
272+
{registry.scriptTags()}
273+
</head>
274+
)
275+
expect(head.find('script').prop('src')).to.equal('SSR')
270276
})
271277
})

0 commit comments

Comments
 (0)