diff --git a/client/index.js b/client/index.js index dac29da8d1..4d5c847a81 100644 --- a/client/index.js +++ b/client/index.js @@ -6,6 +6,7 @@ import { createRouter } from '../lib/router' import App from '../lib/app' import evalScript from '../lib/eval-script' import { loadGetInitialProps, getURL } from '../lib/utils' +import ErrorDebugComponent from '../lib/error-debug' // Polyfill Promise globally // This is needed because Webpack2's dynamic loading(common chunks) code @@ -39,33 +40,57 @@ export const router = createRouter(pathname, query, getURL(), { }) const headManager = new HeadManager() -const container = document.getElementById('__next') +const appContainer = document.getElementById('__next') +const errorContainer = document.getElementById('__next-error') -export default (onError) => { +export default () => { const emitter = mitt() router.subscribe(({ Component, props, hash, err }) => { - render({ Component, props, err, hash, emitter }, onError) + render({ Component, props, err, hash, emitter }) }) const hash = location.hash.substring(1) - render({ Component, props, hash, err, emitter }, onError) + render({ Component, props, hash, err, emitter }) return emitter } -export async function render (props, onError = renderErrorComponent) { +export async function render (props) { + if (props.err) { + await renderError(props.err) + return + } + try { await doRender(props) } catch (err) { - await onError(err) + if (err.abort) return + await renderError(err) } } -async function renderErrorComponent (err) { - const { pathname, query } = router - const props = await loadGetInitialProps(ErrorComponent, { err, pathname, query }) - await doRender({ Component: ErrorComponent, props, err }) +// This method handles all runtime and debug errors. +// 404 and 500 errors are special kind of errors +// and they are still handle via the main render method. +export async function renderError (error) { + const prod = process.env.NODE_ENV === 'production' + // We need to unmount the current app component because it's + // in the inconsistant state. + // Otherwise, we need to face issues when the issue is fixed and + // it's get notified via HMR + ReactDOM.unmountComponentAtNode(appContainer) + + const errorMessage = `${error.message}\n${error.stack}` + console.error(errorMessage) + + if (prod) { + const initProps = { err: error, pathname, query } + const props = await loadGetInitialProps(ErrorComponent, initProps) + ReactDOM.render(createElement(ErrorComponent, props), errorContainer) + } else { + ReactDOM.render(createElement(ErrorDebugComponent, { error }), errorContainer) + } } async function doRender ({ Component, props, hash, err, emitter }) { @@ -88,7 +113,9 @@ async function doRender ({ Component, props, hash, err, emitter }) { // lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error. lastAppProps = appProps - ReactDOM.render(createElement(App, appProps), container) + // We need to clear any existing runtime error messages + ReactDOM.unmountComponentAtNode(errorContainer) + ReactDOM.render(createElement(App, appProps), appContainer) if (emitter) { emitter.emit('after-reactdom-render', { Component }) diff --git a/client/next-dev.js b/client/next-dev.js index ca1132125b..071ec03df4 100644 --- a/client/next-dev.js +++ b/client/next-dev.js @@ -1,4 +1,5 @@ import evalScript from '../lib/eval-script' +import ReactReconciler from 'react-dom/lib/ReactReconciler' const { __NEXT_DATA__: { errorComponent } } = window const ErrorComponent = evalScript(errorComponent).default @@ -7,12 +8,18 @@ require('react-hot-loader/patch') const next = window.next = require('./') -const emitter = next.default(onError) +const emitter = next.default() -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, emitter }) +// This is a patch to catch most of the errors throw inside React components. +const originalMountComponent = ReactReconciler.mountComponent +ReactReconciler.mountComponent = function (...args) { + try { + return originalMountComponent(...args) + } catch (err) { + next.renderError(err) + err.abort = true + throw err + } } let lastScroll diff --git a/lib/app.js b/lib/app.js index 603d3b8947..2cc4d575c8 100644 --- a/lib/app.js +++ b/lib/app.js @@ -17,7 +17,7 @@ export default class App extends Component { } render () { - const { Component, props, hash, err, router } = this.props + const { Component, props, hash, router } = this.props const url = createUrl(router) // If there no component exported we can't proceed. // We'll tackle that here. @@ -28,7 +28,6 @@ export default class App extends Component { return