Skip to content

Commit

Permalink
Handle errors of React lifecycle methods (#661)
Browse files Browse the repository at this point in the history
* handle errors of lifecycle methods

* handle errors of render method
  • Loading branch information
nkzawa authored and rauchg committed Jan 5, 2017
1 parent e01056d commit 8570d19
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 6 deletions.
9 changes: 8 additions & 1 deletion client/next-dev.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import 'react-hot-loader/patch'
import * as next from './next'
import patch from './patch-react'

// apply patch first
patch((err) => {
console.error(err)
next.renderError(err)
})

const next = require('./next')
window.next = next
30 changes: 27 additions & 3 deletions client/next.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createElement } from 'react'
import { render } from 'react-dom'
import ReactDOM from 'react-dom'
import HeadManager from './head-manager'
import { rehydrate } from '../lib/css'
import { createRouter } from '../lib/router'
Expand Down Expand Up @@ -29,7 +29,31 @@ export const router = createRouter(pathname, query, {

const headManager = new HeadManager()
const container = document.getElementById('__next')
const appProps = { Component, props, router, headManager }
const defaultProps = { Component, ErrorComponent, props, router, headManager }

if (ids && ids.length) rehydrate(ids)
render(createElement(App, appProps), container)

render()

export function render (props = {}) {
try {
doRender(props)
} catch (err) {
renderError(err)
}
}

export async function renderError (err) {
const { pathname, query } = router
const props = await ErrorComponent.getInitialProps({ err, pathname, query })
try {
doRender({ Component: ErrorComponent, props })
} catch (err2) {
console.error(err2)
}
}

function doRender (props) {
const appProps = { ...defaultProps, ...props }
ReactDOM.render(createElement(App, appProps), container)
}
62 changes: 62 additions & 0 deletions client/patch-react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// monkeypatch React for fixing https://github.com/facebook/react/issues/2461
// based on https://gist.github.com/Aldredcz/4d63b0a9049b00f54439f8780be7f0d8

import React from 'react'

let patched = false

export default (handleError = () => {}) => {
if (patched) {
throw new Error('React is already monkeypatched')
}

patched = true

const { createElement } = React

React.createElement = function (Component, ...rest) {
if (typeof Component === 'function') {
const { prototype } = Component
if (prototype && prototype.render) {
prototype.render = wrapRender(prototype.render)
} else {
// stateless component
Component = wrapRender(Component)
}
}

return createElement.call(this, Component, ...rest)
}

const { Component: { prototype: componentPrototype } } = React
const { forceUpdate } = componentPrototype

componentPrototype.forceUpdate = function (...args) {
if (this.render) {
this.render = wrapRender(this.render)
}
return forceUpdate.apply(this, args)
}

function wrapRender (render) {
if (render.__wrapped) {
return render.__wrapped
}

const _render = function (...args) {
try {
return render.apply(this, args)
} catch (err) {
handleError(err)
return null
}
}

// copy all properties
Object.assign(_render, render)

render.__wrapped = _render.__wrapped = _render

return _render
}
}
19 changes: 17 additions & 2 deletions lib/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default class App extends Component {
try {
this.setState(state)
} catch (err) {
console.error(err)
this.handleError(err)
}
}

Expand All @@ -37,7 +37,7 @@ export default class App extends Component {
try {
this.setState(state)
} catch (err) {
console.error(err)
this.handleError(err)
}
})
}
Expand All @@ -58,6 +58,21 @@ export default class App extends Component {
<Component {...props} />
</AppContainer>
}

async handleError (err) {
console.error(err)

const { router, ErrorComponent } = this.props
const { pathname, query } = router
const props = await ErrorComponent.getInitialProps({ err, pathname, query })
const state = propsToState({ Component: ErrorComponent, props, router })

try {
this.setState(state)
} catch (err2) {
console.error(err2)
}
}
}

function propsToState (props) {
Expand Down

0 comments on commit 8570d19

Please sign in to comment.