Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't discard component state on error #741

Merged
merged 3 commits into from
Jan 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions client/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { createElement } from 'react'
import ReactDOM from 'react-dom'
import HeadManager from './head-manager'
import { rehydrate } from '../lib/css'
import { createRouter } from '../lib/router'
import App from '../lib/app'
import evalScript from '../lib/eval-script'

const {
__NEXT_DATA__: {
component,
errorComponent,
props,
ids,
err,
pathname,
query
}
} = window

const Component = evalScript(component).default
const ErrorComponent = evalScript(errorComponent).default
let lastAppProps

export const router = createRouter(pathname, query, {
Component,
ErrorComponent,
err
})

const headManager = new HeadManager()
const container = document.getElementById('__next')

export default (onError) => {
if (ids && ids.length) rehydrate(ids)

router.subscribe(({ Component, props, err }) => {
render({ Component, props, err }, onError)
})

render({ Component, props, err }, onError)
}

export async function render (props, onError = renderErrorComponent) {
try {
await doRender(props)
} catch (err) {
await onError(err)
}
}

async function renderErrorComponent (err) {
const { pathname, query } = router
const props = await getInitialProps(ErrorComponent, { err, pathname, query })
await doRender({ Component: ErrorComponent, props, err })
}

async function doRender ({ Component, props, err }) {
if (!props && Component &&
Component !== ErrorComponent &&
lastAppProps.Component === ErrorComponent) {
// fetch props if ErrorComponent was replaced with a page component by HMR
const { pathname, query } = router
props = await getInitialProps(Component, { err, pathname, query })
}

Component = Component || lastAppProps.Component
props = props || lastAppProps.props

const appProps = { Component, props, err, router, headManager }
lastAppProps = appProps
ReactDOM.render(createElement(App, appProps), container)
}

function getInitialProps (Component, ctx) {
return Component.getInitialProps ? Component.getInitialProps(ctx) : {}
}
16 changes: 13 additions & 3 deletions client/next-dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@ import patch from './patch-react'
// apply patch first
patch((err) => {
console.error(err)
next.renderError(err)

Promise.resolve().then(() => {
onError(err)
})
})

require('react-hot-loader/patch')

const next = require('./next')
window.next = next
const next = window.next = require('./')

next.default(onError)

function onError (err) {
// just show the debug screen but don't render ErrorComponent
// so that the current component doesn't lose props
next.render({ err })
}
60 changes: 2 additions & 58 deletions client/next.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,3 @@
import { createElement } from 'react'
import ReactDOM from 'react-dom'
import HeadManager from './head-manager'
import { rehydrate } from '../lib/css'
import { createRouter } from '../lib/router'
import App from '../lib/app'
import evalScript from '../lib/eval-script'
import next from './'

const {
__NEXT_DATA__: {
component,
errorComponent,
props,
ids,
err,
pathname,
query
}
} = window

const Component = evalScript(component).default
const ErrorComponent = evalScript(errorComponent).default

export const router = createRouter(pathname, query, {
Component,
ErrorComponent,
ctx: { err }
})

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

if (ids && ids.length) rehydrate(ids)

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)
}
next()
10 changes: 5 additions & 5 deletions client/webpack-hot-middleware-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ const handlers = {
reload (route) {
if (route === '/_error') {
for (const r of Object.keys(Router.components)) {
const { Component } = Router.components[r]
if (Component.__route === '/_error-debug') {
// reload all '/_error-debug'
const { err } = Router.components[r]
if (err) {
// reload all error routes
// which are expected to be errors of '/_error' routes
Router.reload(r)
}
Expand All @@ -29,8 +29,8 @@ const handlers = {
return
}

const { Component } = Router.components[route] || {}
if (Component && Component.__route === '/_error-debug') {
const { err } = Router.components[route] || {}
if (err) {
// reload to recover from runtime errors
Router.reload(route)
}
Expand Down
87 changes: 26 additions & 61 deletions lib/app.js
Original file line number Diff line number Diff line change
@@ -1,83 +1,53 @@
import React, { Component, PropTypes } from 'react'
import { AppContainer } from 'react-hot-loader'
import shallowEquals from './shallow-equals'
import { warn } from './utils'

const ErrorDebug = process.env.NODE_ENV === 'production'
? null : require('./error-debug').default

export default class App extends Component {
static childContextTypes = {
router: PropTypes.object,
headManager: PropTypes.object
}

constructor (props) {
super(props)
this.state = propsToState(props)
this.close = null
}

componentWillReceiveProps (nextProps) {
const state = propsToState(nextProps)
try {
this.setState(state)
} catch (err) {
this.handleError(err)
}
getChildContext () {
const { headManager } = this.props
return { headManager }
}

componentDidMount () {
const { router } = this.props

this.close = router.subscribe((data) => {
const props = data.props || this.state.props
const state = propsToState({
...data,
props,
router
})
render () {
const { Component, props, err, router } = this.props
const containerProps = { Component, props, router }

try {
this.setState(state)
} catch (err) {
this.handleError(err)
}
})
return <div>
<Container {...containerProps} />
{ErrorDebug && err ? <ErrorDebug err={err} /> : null}
</div>
}

componentWillUnmount () {
if (this.close) this.close()
}
}

getChildContext () {
const { router, headManager } = this.props
return { router, headManager }
class Container extends Component {
shouldComponentUpdate (nextProps) {
// need this check not to rerender component which has already thrown an error
return !shallowEquals(this.props, nextProps)
}

render () {
const { Component, props } = this.state
const { Component, props, router } = this.props
const url = createUrl(router)

// includes AppContainer which bypasses shouldComponentUpdate method
// https://github.com/gaearon/react-hot-loader/issues/442
return <AppContainer>
<Component {...props} />
<Component {...props} url={url} />
</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) {
const { Component, router } = props
const url = {
function createUrl (router) {
return {
query: router.query,
pathname: router.pathname,
back: () => router.back(),
Expand All @@ -98,9 +68,4 @@ function propsToState (props) {
return router.replace(replaceRoute, replaceUrl)
}
}

return {
Component,
props: { ...props.props, url }
}
}
67 changes: 67 additions & 0 deletions lib/error-debug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react'
import ansiHTML from 'ansi-html'
import Head from './head'

export default ({ err: { name, message, stack, module } }) => (
<div style={styles.errorDebug}>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
</Head>
{module ? <div style={styles.heading}>Error in {module.rawRequest}</div> : null}
{
name === 'ModuleBuildError'
? <pre style={styles.message} dangerouslySetInnerHTML={{ __html: ansiHTML(encodeHtml(message)) }} />
: <pre style={styles.message}>{stack}</pre>
}
</div>
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_error-debug is not a page component anymore.


const styles = {
errorDebug: {
background: '#a6004c',
boxSizing: 'border-box',
overflow: 'auto',
padding: '16px',
position: 'fixed',
left: 0,
right: 0,
top: 0,
bottom: 0,
zIndex: 9999
},

message: {
fontFamily: '"SF Mono", "Roboto Mono", "Fira Mono", menlo-regular, monospace',
fontSize: '10px',
color: '#fbe7f1',
margin: 0,
whiteSpace: 'pre-wrap',
wordWrap: 'break-word'
},

heading: {
fontFamily: '-apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Fira Sans", Avenir, "Helvetica Neue", "Lucida Grande", sans-serif',
fontSize: '13px',
fontWeight: 'bold',
color: '#ff84bf',
marginBottom: '20px'
}
}

const encodeHtml = str => {
return str.replace(/</g, '&lt;').replace(/>/g, '&gt;')
}

// see color definitions of babel-code-frame:
// https://github.com/babel/babel/blob/master/packages/babel-code-frame/src/index.js

ansiHTML.setColors({
reset: ['fff', 'a6004c'],
darkgrey: 'e54590',
yellow: 'ee8cbb',
green: 'f2a2c7',
magenta: 'fbe7f1',
blue: 'fff',
cyan: 'ef8bb9',
red: 'fff'
})
Loading