Skip to content

Commit 8570d19

Browse files
nkzawarauchg
authored andcommitted
Handle errors of React lifecycle methods (vercel#661)
* handle errors of lifecycle methods * handle errors of render method
1 parent e01056d commit 8570d19

4 files changed

Lines changed: 114 additions & 6 deletions

File tree

client/next-dev.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import 'react-hot-loader/patch'
2-
import * as next from './next'
2+
import patch from './patch-react'
33

4+
// apply patch first
5+
patch((err) => {
6+
console.error(err)
7+
next.renderError(err)
8+
})
9+
10+
const next = require('./next')
411
window.next = next

client/next.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createElement } from 'react'
2-
import { render } from 'react-dom'
2+
import ReactDOM from 'react-dom'
33
import HeadManager from './head-manager'
44
import { rehydrate } from '../lib/css'
55
import { createRouter } from '../lib/router'
@@ -29,7 +29,31 @@ export const router = createRouter(pathname, query, {
2929

3030
const headManager = new HeadManager()
3131
const container = document.getElementById('__next')
32-
const appProps = { Component, props, router, headManager }
32+
const defaultProps = { Component, ErrorComponent, props, router, headManager }
3333

3434
if (ids && ids.length) rehydrate(ids)
35-
render(createElement(App, appProps), container)
35+
36+
render()
37+
38+
export function render (props = {}) {
39+
try {
40+
doRender(props)
41+
} catch (err) {
42+
renderError(err)
43+
}
44+
}
45+
46+
export async function renderError (err) {
47+
const { pathname, query } = router
48+
const props = await ErrorComponent.getInitialProps({ err, pathname, query })
49+
try {
50+
doRender({ Component: ErrorComponent, props })
51+
} catch (err2) {
52+
console.error(err2)
53+
}
54+
}
55+
56+
function doRender (props) {
57+
const appProps = { ...defaultProps, ...props }
58+
ReactDOM.render(createElement(App, appProps), container)
59+
}

client/patch-react.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// monkeypatch React for fixing https://github.com/facebook/react/issues/2461
2+
// based on https://gist.github.com/Aldredcz/4d63b0a9049b00f54439f8780be7f0d8
3+
4+
import React from 'react'
5+
6+
let patched = false
7+
8+
export default (handleError = () => {}) => {
9+
if (patched) {
10+
throw new Error('React is already monkeypatched')
11+
}
12+
13+
patched = true
14+
15+
const { createElement } = React
16+
17+
React.createElement = function (Component, ...rest) {
18+
if (typeof Component === 'function') {
19+
const { prototype } = Component
20+
if (prototype && prototype.render) {
21+
prototype.render = wrapRender(prototype.render)
22+
} else {
23+
// stateless component
24+
Component = wrapRender(Component)
25+
}
26+
}
27+
28+
return createElement.call(this, Component, ...rest)
29+
}
30+
31+
const { Component: { prototype: componentPrototype } } = React
32+
const { forceUpdate } = componentPrototype
33+
34+
componentPrototype.forceUpdate = function (...args) {
35+
if (this.render) {
36+
this.render = wrapRender(this.render)
37+
}
38+
return forceUpdate.apply(this, args)
39+
}
40+
41+
function wrapRender (render) {
42+
if (render.__wrapped) {
43+
return render.__wrapped
44+
}
45+
46+
const _render = function (...args) {
47+
try {
48+
return render.apply(this, args)
49+
} catch (err) {
50+
handleError(err)
51+
return null
52+
}
53+
}
54+
55+
// copy all properties
56+
Object.assign(_render, render)
57+
58+
render.__wrapped = _render.__wrapped = _render
59+
60+
return _render
61+
}
62+
}

lib/app.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default class App extends Component {
1919
try {
2020
this.setState(state)
2121
} catch (err) {
22-
console.error(err)
22+
this.handleError(err)
2323
}
2424
}
2525

@@ -37,7 +37,7 @@ export default class App extends Component {
3737
try {
3838
this.setState(state)
3939
} catch (err) {
40-
console.error(err)
40+
this.handleError(err)
4141
}
4242
})
4343
}
@@ -58,6 +58,21 @@ export default class App extends Component {
5858
<Component {...props} />
5959
</AppContainer>
6060
}
61+
62+
async handleError (err) {
63+
console.error(err)
64+
65+
const { router, ErrorComponent } = this.props
66+
const { pathname, query } = router
67+
const props = await ErrorComponent.getInitialProps({ err, pathname, query })
68+
const state = propsToState({ Component: ErrorComponent, props, router })
69+
70+
try {
71+
this.setState(state)
72+
} catch (err2) {
73+
console.error(err2)
74+
}
75+
}
6176
}
6277

6378
function propsToState (props) {

0 commit comments

Comments
 (0)