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
- {ErrorDebug && err ? : null}
} } diff --git a/server/document.js b/server/document.js index 9d25fd6c2f..6c698da29e 100644 --- a/server/document.js +++ b/server/document.js @@ -4,9 +4,9 @@ import flush from 'styled-jsx/server' export default class Document extends Component { static getInitialProps ({ renderPage }) { - const {html, head} = renderPage() + const { html, head, errorHtml } = renderPage() const styles = flush() - return { html, head, styles } + return { html, head, errorHtml, styles } } static childContextTypes = { @@ -49,8 +49,13 @@ export class Main extends Component { } render () { - const { html } = this.context._documentProps - return
+ const { html, errorHtml } = this.context._documentProps + return ( +
+
+
+
+ ) } } diff --git a/server/render.js b/server/render.js index 36c18994e3..8a39a69dcc 100644 --- a/server/render.js +++ b/server/render.js @@ -9,6 +9,7 @@ import { Router } from '../lib/router' import { loadGetInitialProps } from '../lib/utils' import Head, { defaultHead } from '../lib/head' import App from '../lib/app' +import ErrorDebug from '../lib/error-debug' export async function render (req, res, pathname, query, opts) { const html = await renderToHTML(req, res, pathname, opts) @@ -67,7 +68,6 @@ async function doRender (req, res, pathname, query, { const app = createElement(App, { Component, props, - err: dev ? err : null, router: new Router(pathname, query) }) @@ -75,12 +75,18 @@ async function doRender (req, res, pathname, query, { let html let head + let errorHtml = '' try { html = render(app) } finally { head = Head.rewind() || defaultHead() } - return { html, head } + + if (err && dev) { + errorHtml = render(createElement(ErrorDebug, { error: err })) + } + + return { html, head, errorHtml } } const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage })