diff --git a/client/index.js b/client/index.js
new file mode 100644
index 0000000000000..3c1fab58db77b
--- /dev/null
+++ b/client/index.js
@@ -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) : {}
+}
diff --git a/client/next-dev.js b/client/next-dev.js
index 8453ebabd9d43..2ae0ba06f9bb3 100644
--- a/client/next-dev.js
+++ b/client/next-dev.js
@@ -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 })
+}
diff --git a/client/next.js b/client/next.js
index a1897675e5b27..5400a409161c3 100644
--- a/client/next.js
+++ b/client/next.js
@@ -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()
diff --git a/client/webpack-hot-middleware-client.js b/client/webpack-hot-middleware-client.js
index adfe4081a7929..7dfc49492c12b 100644
--- a/client/webpack-hot-middleware-client.js
+++ b/client/webpack-hot-middleware-client.js
@@ -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)
}
@@ -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)
}
diff --git a/lib/app.js b/lib/app.js
index 5367ea05a619d..02864563a1fa0 100644
--- a/lib/app.js
+++ b/lib/app.js
@@ -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
+
+ {ErrorDebug && err ? : null}
+
}
- 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
-
+
}
-
- 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(),
@@ -98,9 +68,4 @@ function propsToState (props) {
return router.replace(replaceRoute, replaceUrl)
}
}
-
- return {
- Component,
- props: { ...props.props, url }
- }
}
diff --git a/lib/error-debug.js b/lib/error-debug.js
new file mode 100644
index 0000000000000..04d58180d0fa7
--- /dev/null
+++ b/lib/error-debug.js
@@ -0,0 +1,67 @@
+import React from 'react'
+import ansiHTML from 'ansi-html'
+import Head from './head'
+
+export default ({ err: { name, message, stack, module } }) => (
+
+
+
+
+ {module ?
Error in {module.rawRequest}
: null}
+ {
+ name === 'ModuleBuildError'
+ ?
+ :
{stack}
+ }
+
+)
+
+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, '>')
+}
+
+// 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'
+})
diff --git a/lib/router/router.js b/lib/router/router.js
index 02ae798254cb9..2cc6e1f6bc3d8 100644
--- a/lib/router/router.js
+++ b/lib/router/router.js
@@ -5,13 +5,13 @@ import { EventEmitter } from 'events'
import { reloadIfPrefetched } from '../prefetch'
export default class Router extends EventEmitter {
- constructor (pathname, query, { Component, ErrorComponent, ctx } = {}) {
+ constructor (pathname, query, { Component, ErrorComponent, err } = {}) {
super()
// represents the current component key
this.route = toRoute(pathname)
// set up the component cache (by route keys)
- this.components = { [this.route]: { Component, ctx } }
+ this.components = { [this.route]: { Component, err } }
this.ErrorComponent = ErrorComponent
this.pathname = pathname
@@ -166,17 +166,18 @@ export default class Router extends EventEmitter {
const routeInfo = {}
try {
- const data = routeInfo.data = await this.fetchComponent(route)
- const ctx = { ...data.ctx, pathname, query }
- routeInfo.props = await this.getInitialProps(data.Component, ctx)
+ const { Component, err, xhr } = routeInfo.data = await this.fetchComponent(route)
+ const ctx = { err, xhr, pathname, query }
+ routeInfo.props = await this.getInitialProps(Component, ctx)
} catch (err) {
if (err.cancelled) {
return { error: err }
}
- const data = routeInfo.data = { Component: this.ErrorComponent, ctx: { err } }
- const ctx = { ...data.ctx, pathname, query }
- routeInfo.props = await this.getInitialProps(data.Component, ctx)
+ const Component = this.ErrorComponent
+ routeInfo.data = { Component, err }
+ const ctx = { err, pathname, query }
+ routeInfo.props = await this.getInitialProps(Component, ctx)
routeInfo.error = err
console.error(err)
@@ -213,10 +214,7 @@ export default class Router extends EventEmitter {
const url = `/_next/pages${route}`
const xhr = loadComponent(url, (err, data) => {
if (err) return reject(err)
- resolve({
- Component: data.Component,
- ctx: { xhr, err: data.err }
- })
+ resolve({ ...data, xhr })
})
})
diff --git a/pages/_error-debug.js b/pages/_error-debug.js
deleted file mode 100644
index 42c1a320e446c..0000000000000
--- a/pages/_error-debug.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import React from 'react'
-import Head from 'next/head'
-import ansiHTML from 'ansi-html'
-
-export default class ErrorDebug extends React.Component {
- static getInitialProps ({ err }) {
- const { name, message, stack, module } = err
- return { name, message, stack, path: module ? module.rawRequest : null }
- }
-
- render () {
- const { name, message, stack, path } = this.props
-
- return
-
-
-
- {path ?
Error in {path}
: null}
- {
- name === 'ModuleBuildError'
- ?
- :
{stack}
- }
-
-
-
- }
-}
-
-const encodeHtml = str => {
- return str.replace(//g, '>')
-}
-
-// 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'
-})
diff --git a/server/build/webpack.js b/server/build/webpack.js
index f3fc98053748f..dc4abc691d935 100644
--- a/server/build/webpack.js
+++ b/server/build/webpack.js
@@ -17,7 +17,6 @@ import getConfig from '../config'
const documentPage = join('pages', '_document.js')
const defaultPages = [
'_error.js',
- '_error-debug.js',
'_document.js'
]
diff --git a/server/render.js b/server/render.js
index 6d26eaeec89f8..3b05c00df5909 100644
--- a/server/render.js
+++ b/server/render.js
@@ -27,8 +27,7 @@ export async function renderError (err, req, res, pathname, query, opts) {
}
export function renderErrorToHTML (err, req, res, pathname, query, opts = {}) {
- const page = err && opts.dev ? '/_error-debug' : '/_error'
- return doRender(req, res, pathname, query, { ...opts, err, page })
+ return doRender(req, res, pathname, query, { ...opts, err, page: '_error' })
}
async function doRender (req, res, pathname, query, {
@@ -54,7 +53,7 @@ async function doRender (req, res, pathname, query, {
] = await Promise.all([
Component.getInitialProps ? Component.getInitialProps(ctx) : {},
readPage(join(dir, '.next', 'bundles', 'pages', page)),
- readPage(join(dir, '.next', 'bundles', 'pages', dev ? '_error-debug' : '_error'))
+ readPage(join(dir, '.next', 'bundles', 'pages', '_error'))
])
// the response might be finshed on the getinitialprops call
@@ -64,6 +63,7 @@ async function doRender (req, res, pathname, query, {
const app = createElement(App, {
Component,
props,
+ err,
router: new Router(pathname, query)
})
@@ -104,8 +104,7 @@ export async function renderJSON (req, res, page, { dir = process.cwd() } = {})
}
export async function renderErrorJSON (err, req, res, { dir = process.cwd(), dev = false } = {}) {
- const page = err && dev ? '/_error-debug' : '/_error'
- const component = await readPage(join(dir, '.next', 'bundles', 'pages', page))
+ const component = await readPage(join(dir, '.next', 'bundles', 'pages', '_error'))
sendJSON(res, {
component,
diff --git a/test/integration.test.js b/test/integration.test.js
index a7b04491f537a..cecbf383fdb6d 100644
--- a/test/integration.test.js
+++ b/test/integration.test.js
@@ -63,7 +63,7 @@ describe('integration tests', () => {
test('error', async () => {
const html = await render('/error')
- expect(html).toMatch(/Error: This is an expected error\n[^]+<\/pre>/)
+ expect(html).toMatch(/Error: This is an expected error\n[^]+<\/pre>/)
})
test('error 404', async () => {