Skip to content

Commit 7fd3816

Browse files
committed
feat: add SSR support
1 parent cc3bc97 commit 7fd3816

File tree

3 files changed

+124
-14
lines changed

3 files changed

+124
-14
lines changed

README.md

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@ it will create the script tag you've requested.
1515
is a pre-existing `<script>` tag for that URL that it didn't create. If `src`
1616
prop changes, it will load that new URL.
1717

18-
## Version notes
18+
# Version notes
1919

2020
- supports React 15 or 16
2121
- if building for legacy browsers with a bundler like Webpack that supports the
2222
`module` field of `package.json`, you will probably need to add a rule to
2323
transpile this package.
2424

25-
## Installation
25+
# Installation
2626

2727
```sh
2828
npm install --save react-render-props-script-loader
2929
```
3030

31-
## Example
31+
# Example
3232

3333
```js
3434
import * as React from 'react'
@@ -52,9 +52,13 @@ export const MapViewContainer = props => (
5252
)
5353
```
5454

55-
## API
55+
# API
5656

57-
The package exports a single component with the following props:
57+
## `ScriptLoader`
58+
59+
```js
60+
import ScriptLoader from 'react-render-props-script-loader'
61+
```
5862

5963
### `src` (**required** `string`)
6064

@@ -73,3 +77,48 @@ script
7377

7478
The render function. It will be called with an object having the following
7579
props, and may return your desired content to display:
80+
81+
```js
82+
{
83+
loading: boolean,
84+
loaded: boolean,
85+
error: ?Error,
86+
promise: ?Promise<any>,
87+
}
88+
```
89+
90+
## Server-Side Rendering
91+
92+
```js
93+
import {
94+
ScriptsRegistry,
95+
ScriptsRegistryContext,
96+
} from 'react-render-props-script-loader'
97+
```
98+
99+
On the server, create an instance of `ScriptsRegistry` and put it on the app's
100+
context:
101+
102+
```js
103+
const registry = new ScriptsRegistry()
104+
105+
const body = ReactDOM.renderToString(
106+
<ScriptsRegistryContext.Provider value={registry}>
107+
<App />
108+
</ScriptsRegistryContext.Provider>
109+
)
110+
```
111+
112+
Then render `registry.scriptTags()` in your head element:
113+
114+
```js
115+
const html = (
116+
<html className="default">
117+
<head>
118+
...
119+
{registry.scriptTags()}
120+
</head>
121+
...
122+
</html>
123+
)
124+
```

src/index.js

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@ export type State = {
1313

1414
export type Props = {
1515
src: string,
16+
type?: ?string,
1617
onLoad?: ?() => any,
1718
onError?: ?(error: Error) => any,
1819
children?: ?(state: State) => ?React.Node,
1920
}
2021

22+
export const ScriptsRegistryContext: React.Context<?ScriptsRegistry> = React.createContext(
23+
null
24+
)
25+
2126
export default class ScriptLoader extends React.PureComponent<Props, State> {
2227
state = getState(this.props)
2328
promise: ?Promise<void>
@@ -68,12 +73,46 @@ export default class ScriptLoader extends React.PureComponent<Props, State> {
6873
this.promise = null
6974
}
7075

71-
render(): React.Node | null {
72-
const { children } = this.props
73-
if (children) {
74-
const result = children({ ...this.state })
75-
return result == null ? null : result
76-
}
77-
return null
76+
render(): React.Node {
77+
const { children, type, src } = this.props
78+
return (
79+
<ScriptsRegistryContext.Consumer>
80+
{(context: ?ScriptsRegistry) => {
81+
if (context) {
82+
context.scripts.push({ type, src })
83+
if (!children) return <React.Fragment />
84+
const result = children({
85+
loading: true,
86+
loaded: false,
87+
error: null,
88+
promise: new Promise(() => {}),
89+
})
90+
return result == null ? null : result
91+
}
92+
if (children) {
93+
const result = children({ ...this.state })
94+
return result == null ? null : result
95+
}
96+
return null
97+
}}
98+
</ScriptsRegistryContext.Consumer>
99+
)
100+
}
101+
}
102+
103+
export class ScriptsRegistry {
104+
scripts: Array<{
105+
type?: ?string,
106+
src: string,
107+
}> = []
108+
109+
scriptTags(): React.Node {
110+
return (
111+
<React.Fragment>
112+
{this.scripts.map(({ type, src }, index) => (
113+
<script key={index} type={type} src={src} />
114+
))}
115+
</React.Fragment>
116+
)
78117
}
79118
}

test/index.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
/* eslint-env browser */
44

5-
import { describe, it } from 'mocha'
5+
import { describe, it, afterEach } from 'mocha'
66
import * as React from 'react'
77
import { mount } from 'enzyme'
88
import { expect } from 'chai'
99
import sinon from 'sinon'
1010

11-
import ScriptLoader from '../src'
11+
import ScriptLoader, { ScriptsRegistryContext, ScriptsRegistry } from '../src'
1212
import loadScript from '../src/loadScript'
1313

1414
describe('ScriptLoader', () => {
@@ -247,3 +247,25 @@ describe(`loadScript`, function() {
247247
}
248248
})
249249
})
250+
describe(`SSR`, function() {
251+
it(`works`, function() {
252+
this.timeout(10000)
253+
const render = sinon.spy(() => 'hello')
254+
const registry = new ScriptsRegistry()
255+
mount(
256+
<ScriptsRegistryContext.Provider value={registry}>
257+
<ScriptLoader src="foo" id="scriptId">
258+
{render}
259+
</ScriptLoader>
260+
</ScriptsRegistryContext.Provider>
261+
)
262+
const comp = mount(registry.scriptTags())
263+
264+
expect(render.lastCall.lastArg).to.containSubset({
265+
loading: true,
266+
loaded: false,
267+
error: null,
268+
})
269+
expect(comp.find('script').prop('src')).to.equal('foo')
270+
})
271+
})

0 commit comments

Comments
 (0)