Skip to content

Commit e8d6287

Browse files
authored
Add support for React 17's React.lazy changes, Error Boundaries, and refactor render loop (#61)
* Add initial error boundary implementation * Collapse flushFrames implementation * Remove external object-is dependency * Upgrade dependencies * Inline react-is symbols to remove peer dependency * Upgrade React and update React.lazy support * Protect against render loops caused by errors * Add tests for error boundaries * Fix typo on suspense error frames * Remove react-apollo section This isn't supported anymore as Apollo removed their component methods that would allow prepass to convert their Query components to suspense-like primitives. * v1.3.0-rc.0
1 parent e2020e1 commit e8d6287

23 files changed

+2451
-2192
lines changed

README.md

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ import { renderToString } from 'react-dom/server'
8181

8282
import ssrPrepass from 'react-ssr-prepass'
8383

84-
const renderApp = async App => {
84+
const renderApp = async (App) => {
8585
const element = createElement(App)
8686
await ssrPrepass(element)
8787

@@ -113,30 +113,6 @@ data rehydration. In most cases it's fine to collect data from your cache
113113
or store after running `ssrPrepass`, turn it into JSON, and send it
114114
down in your HTML result.
115115

116-
## Examples & Recipes
117-
118-
### Usage with `react-apollo`
119-
120-
Instead of using `react-apollo`'s own `getDataFromTree` function, `react-ssr-prepass`
121-
can be used instead. For this to work, we will have to write a visitor function
122-
that knows how to suspend on `react-apollo`'s `Query` component.
123-
124-
Luckily this is quite simple, since all we need to do is call the `fetchData`
125-
method on the `Query` component's instance.
126-
127-
```js
128-
ssrPrepass(<App />, (_element, instance) => {
129-
if (instance !== undefined && typeof instance.fetchData === 'function') {
130-
return instance.fetchData()
131-
}
132-
})
133-
```
134-
135-
Since we're now calling `fetchData` when it exists, which returns a `Promise`
136-
already, `ssrPrepass` will suspend on `<Query>` components.
137-
138-
[More information can be found in Apollo's own docs](https://www.apollographql.com/docs/react/features/server-side-rendering.html#getDataFromTree)
139-
140116
## Prior Art
141117

142118
This library is (luckily) not a reimplementation from scratch of

package.json

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-ssr-prepass",
3-
"version": "1.2.1",
3+
"version": "1.3.0-rc.0",
44
"description": "A custom partial React SSR renderer for prefetching and suspense",
55
"main": "index.js",
66
"author": "Phil Plückthun <phil.pluckthun@formidable.com>",
@@ -35,11 +35,6 @@
3535
"@babel/preset-react"
3636
]
3737
},
38-
"jest": {
39-
"globals": {
40-
"__DEV__": true
41-
}
42-
},
4338
"lint-staged": {
4439
"**/*.js": [
4540
"flow focus-check",
@@ -55,38 +50,33 @@
5550
}
5651
},
5752
"peerDependencies": {
58-
"react": "^16.8.0",
59-
"react-is": "^16.8.0"
60-
},
61-
"dependencies": {
62-
"object-is": "^1.1.2"
53+
"react": "^16.8.0 || ^17.0.0"
6354
},
6455
"devDependencies": {
65-
"@ampproject/rollup-plugin-closure-compiler": "^0.25.2",
66-
"@babel/core": "^7.9.0",
67-
"@babel/plugin-transform-flow-strip-types": "^7.9.0",
68-
"@babel/plugin-transform-object-assign": "^7.8.3",
69-
"@babel/preset-env": "^7.9.5",
70-
"@babel/preset-flow": "^7.9.0",
71-
"@babel/preset-react": "^7.9.4",
56+
"@ampproject/rollup-plugin-closure-compiler": "^0.26.0",
57+
"@babel/core": "^7.12.3",
58+
"@babel/plugin-transform-flow-strip-types": "^7.12.1",
59+
"@babel/plugin-transform-object-assign": "^7.12.1",
60+
"@babel/preset-env": "^7.12.1",
61+
"@babel/preset-flow": "^7.12.1",
62+
"@babel/preset-react": "^7.12.5",
7263
"@rollup/plugin-buble": "^0.21.3",
73-
"@rollup/plugin-commonjs": "^11.1.0",
74-
"@rollup/plugin-node-resolve": "^7.1.3",
75-
"babel-plugin-closure-elimination": "^1.3.0",
64+
"@rollup/plugin-commonjs": "^16.0.0",
65+
"@rollup/plugin-node-resolve": "^10.0.0",
66+
"babel-plugin-closure-elimination": "^1.3.2",
7667
"babel-plugin-transform-async-to-promises": "^0.8.15",
7768
"codecov": "^3.6.5",
78-
"flow-bin": "^0.122.0",
79-
"husky": "^4.2.5",
80-
"jest": "^25.3.0",
81-
"lint-staged": "^10.1.3",
69+
"flow-bin": "0.122.0",
70+
"husky": "^4.3.0",
71+
"jest": "^26.6.3",
72+
"lint-staged": "^10.5.1",
8273
"npm-run-all": "^4.1.5",
83-
"prettier": "^2.0.4",
84-
"react": "^16.13.1",
85-
"react-dom": "^16.13.1",
86-
"react-is": "^16.13.1",
87-
"rollup": "^2.6.1",
74+
"prettier": "^2.1.2",
75+
"react": "^17.0.1",
76+
"react-dom": "^17.0.1",
77+
"rollup": "^2.33.1",
8878
"rollup-plugin-babel": "^4.4.0",
8979
"rollup-plugin-replace": "^2.2.0",
90-
"rollup-plugin-terser": "^5.3.0"
80+
"rollup-plugin-terser": "^7.0.2"
9181
}
9282
}

rollup.config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ const externalTest = (id) => {
2929
}
3030

3131
const terserPretty = terser({
32-
sourcemap: true,
3332
warnings: true,
3433
ecma: 5,
3534
keep_fnames: true,
@@ -56,7 +55,6 @@ const terserPretty = terser({
5655
})
5756

5857
const terserMinified = terser({
59-
sourcemap: true,
6058
warnings: true,
6159
ecma: 5,
6260
ie8: false,

src/__tests__/element.test.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,6 @@ describe('typeOf', () => {
2626
})
2727
).toBe(REACT_PORTAL_TYPE)
2828

29-
expect(
30-
typeOf({
31-
$$typeof: is.Element,
32-
type: is.ConcurrentMode
33-
})
34-
).toBe(REACT_CONCURRENT_MODE_TYPE)
35-
3629
expect(
3730
typeOf({
3831
$$typeof: is.Element,
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import React, { Component } from 'react'
2+
import renderPrepass from '..'
3+
4+
it('returns to the next componentDidCatch boundary on erroring', () => {
5+
const Throw = jest.fn(() => {
6+
throw new Error()
7+
})
8+
const Inner = jest.fn(() => null)
9+
10+
class Outer extends Component {
11+
constructor() {
12+
super()
13+
this.state = { error: false }
14+
}
15+
16+
componentDidCatch(error) {
17+
this.setState({ error: true })
18+
}
19+
20+
render() {
21+
return this.state.error ? <Inner /> : <Throw />
22+
}
23+
}
24+
25+
const render$ = renderPrepass(<Outer />)
26+
expect(Throw).toHaveBeenCalledTimes(1)
27+
expect(Inner).not.toHaveBeenCalled()
28+
29+
return render$.then(() => {
30+
expect(Inner).toHaveBeenCalledTimes(1)
31+
})
32+
})
33+
34+
it('returns to the next getDerivedStateFromError boundary on erroring', () => {
35+
const Throw = jest.fn(() => {
36+
throw new Error()
37+
})
38+
const Inner = jest.fn(() => null)
39+
40+
class Outer extends Component {
41+
static getDerivedStateFromProps() {
42+
return { error: false }
43+
}
44+
45+
static getDerivedStateFromError() {
46+
return { error: true }
47+
}
48+
49+
render() {
50+
return this.state.error ? <Inner /> : <Throw />
51+
}
52+
}
53+
54+
const render$ = renderPrepass(<Outer />)
55+
expect(Throw).toHaveBeenCalledTimes(1)
56+
expect(Inner).not.toHaveBeenCalled()
57+
58+
return render$.then(() => {
59+
expect(Inner).toHaveBeenCalledTimes(1)
60+
})
61+
})
62+
63+
it('guards against infinite render loops', () => {
64+
const Throw = jest.fn(() => {
65+
throw new Error()
66+
})
67+
68+
class Outer extends Component {
69+
componentDidCatch() {} // NOTE: This doesn't actually recover from errors
70+
render() {
71+
return <Throw />
72+
}
73+
}
74+
75+
return renderPrepass(<Outer />).then(() => {
76+
expect(Throw).toHaveBeenCalledTimes(25)
77+
})
78+
})
79+
80+
it('returns to the next error boundary on a suspense error', () => {
81+
const Inner = jest.fn(() => null)
82+
83+
const Throw = jest.fn(() => {
84+
throw Promise.reject(new Error('Suspense!'))
85+
})
86+
87+
class Outer extends Component {
88+
static getDerivedStateFromProps() {
89+
return { error: false }
90+
}
91+
92+
static getDerivedStateFromError(error) {
93+
expect(error).not.toBeInstanceOf(Promise)
94+
return { error: true }
95+
}
96+
97+
render() {
98+
return this.state.error ? <Inner /> : <Throw />
99+
}
100+
}
101+
102+
const render$ = renderPrepass(<Outer />)
103+
expect(Throw).toHaveBeenCalledTimes(1)
104+
expect(Inner).not.toHaveBeenCalled()
105+
106+
return render$.then(() => {
107+
expect(Inner).toHaveBeenCalledTimes(1)
108+
})
109+
})
110+
111+
it('returns to the next error boundary on a nested error', () => {
112+
const Throw = jest.fn(({ depth }) => {
113+
if (depth >= 4) {
114+
throw new Error('' + depth)
115+
}
116+
117+
return <Throw depth={depth + 1} />
118+
})
119+
120+
class Outer extends Component {
121+
static getDerivedStateFromProps() {
122+
return { error: false }
123+
}
124+
125+
static getDerivedStateFromError(error) {
126+
expect(error.message).toBe('4')
127+
return { error: true }
128+
}
129+
130+
render() {
131+
return !this.state.error ? <Throw depth={1} /> : null
132+
}
133+
}
134+
135+
renderPrepass(<Outer />).then(() => {
136+
expect(Throw).toHaveBeenCalledTimes(4)
137+
})
138+
})
139+
140+
it('always returns to the correct error boundary', () => {
141+
const values = []
142+
143+
const Inner = jest.fn(({ value, depth }) => {
144+
values.push({ value, depth })
145+
return value
146+
})
147+
148+
const Throw = jest.fn(({ value }) => {
149+
throw new Error('' + value)
150+
})
151+
152+
class Outer extends Component {
153+
static getDerivedStateFromProps(props) {
154+
return { value: null }
155+
}
156+
157+
static getDerivedStateFromError(error) {
158+
return { value: error.message }
159+
}
160+
161+
render() {
162+
return [
163+
this.state.value ? (
164+
<Inner value={this.state.value} depth={this.props.depth} />
165+
) : (
166+
<Throw value={this.props.depth} />
167+
),
168+
this.props.depth < 4 ? <Outer depth={this.props.depth + 1} /> : null
169+
]
170+
}
171+
}
172+
173+
return renderPrepass(<Outer depth={1} />).then(() => {
174+
expect(Throw).toHaveBeenCalledTimes(4)
175+
expect(Inner).toHaveBeenCalledTimes(4)
176+
expect(values).toMatchInlineSnapshot(`
177+
Array [
178+
Object {
179+
"depth": 1,
180+
"value": "1",
181+
},
182+
Object {
183+
"depth": 2,
184+
"value": "2",
185+
},
186+
Object {
187+
"depth": 3,
188+
"value": "3",
189+
},
190+
Object {
191+
"depth": 4,
192+
"value": "4",
193+
},
194+
]
195+
`)
196+
})
197+
})

0 commit comments

Comments
 (0)