diff --git a/src/index.js b/src/index.js index 4deebcb9..9c263937 100644 --- a/src/index.js +++ b/src/index.js @@ -59,7 +59,7 @@ function renderToString(vnode, context, opts, inner, isSvgMode) { nodeName = getComponentName(nodeName); } else { - let rendered; + let c, rendered; if (!nodeName.prototype || typeof nodeName.prototype.render!=='function') { // stateless functional components @@ -67,7 +67,7 @@ function renderToString(vnode, context, opts, inner, isSvgMode) { } else { // class-based components - let c = new nodeName(props, context); + c = new nodeName(props, context); // turn off stateful re-rendering: c._dirty = c.__d = true; c.props = props; @@ -81,7 +81,17 @@ function renderToString(vnode, context, opts, inner, isSvgMode) { } } - return renderToString(rendered, context, opts, opts.shallowHighOrder!==false); + try { + return renderToString(rendered, context, opts, opts.shallowHighOrder!==false); + } + catch (error) { + if (c && c.componentDidCatch) { + c.componentDidCatch(error); + rendered = c.render(c.props, c.state, c.context); + return renderToString(rendered, context, opts, opts.shallowHighOrder!==false); + } + throw error; + } } } diff --git a/test/render.js b/test/render.js index 37c145ac..1c69a4fe 100644 --- a/test/render.js +++ b/test/render.js @@ -606,4 +606,258 @@ describe('render', () => { expect(Bar).to.have.been.calledOnce.and.calledWithMatch({ count: 1 }); }); }); + + describe('Error Handling', () => { + it('should invoke ErrorBoundary\'s componentDidCatch from an error thrown in ComponentThatThrows\' getDerivedStateFromProps', () => { + const ERROR_MESSAGE = 'getDerivedStateFromProps error', + THE_ERROR = new Error(ERROR_MESSAGE); + class ComponentThatThrows extends Component { + static getDerivedStateFromProps() { + throw THE_ERROR; + } + componentDidCatch(error) {} + render(props) { + return
; + } + } + class ComponentThatRenders extends Component { + componentDidCatch(error) {} + render(props) { + return
; + } + } + class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { throwError: true }; + } + componentDidCatch(error) { + this.setState({ throwError: false }); + } + render(props, { throwError }) { + return throwError + ? + : ; + } + } + class App extends Component { + componentDidCatch(error) {} + render(props) { + return ; + } + } + + spy(ComponentThatThrows.prototype.constructor, 'getDerivedStateFromProps'); + spy(ComponentThatThrows.prototype, 'componentDidCatch'); + spy(ComponentThatThrows.prototype, 'render'); + spy(ComponentThatRenders.prototype, 'componentDidCatch'); + spy(ComponentThatRenders.prototype, 'render'); + spy(ErrorBoundary.prototype, 'componentDidCatch'); + spy(ErrorBoundary.prototype, 'render'); + spy(App.prototype, 'componentDidCatch'); + spy(App.prototype, 'render'); + + render(); + + // ComponentThatThrows + expect(ComponentThatThrows.prototype.constructor.getDerivedStateFromProps) + .to.have.been.calledOnce + .and.to.throw(Error, ERROR_MESSAGE); + + expect(ComponentThatThrows.prototype.componentDidCatch) + .to.not.have.been.called; + + expect(ComponentThatThrows.prototype.render) + .to.not.have.been.called; + + // ComponentThatRenders + expect(ComponentThatRenders.prototype.componentDidCatch) + .to.not.have.been.called; + + expect(ComponentThatRenders.prototype.render) + .to.have.been.calledOnce + .and.to.not.throw(); + + // ErrorBoundary + expect(ErrorBoundary.prototype.componentDidCatch) + .to.have.been.calledOnce + .and.calledWithExactly(THE_ERROR); + + expect(ErrorBoundary.prototype.render) + .to.have.been.calledTwice; + + // App + expect(App.prototype.render) + .to.have.been.calledOnce; + + expect(App.prototype.componentDidCatch) + .to.not.have.been.called; + }); + + it('should invoke ErrorBoundary\'s componentDidCatch from an error thrown in ComponentThatThrows\' componentWillMount', () => { + const ERROR_MESSAGE = 'componentWillMount error', + THE_ERROR = new Error(ERROR_MESSAGE); + class ComponentThatThrows extends Component { + componentWillMount() { + throw THE_ERROR; + } + componentDidCatch(error) {} + render(props) { + return
; + } + } + class ComponentThatRenders extends Component { + componentDidCatch(error) {} + render(props) { + return
; + } + } + class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { throwError: true }; + } + componentDidCatch(error) { + this.setState({ throwError: false }); + } + render(props, { throwError }) { + return throwError + ? + : ; + } + } + class App extends Component { + componentDidCatch(error) {} + render(props) { + return ; + } + } + + spy(ComponentThatThrows.prototype, 'componentWillMount'); + spy(ComponentThatThrows.prototype, 'componentDidCatch'); + spy(ComponentThatThrows.prototype, 'render'); + spy(ComponentThatRenders.prototype, 'componentDidCatch'); + spy(ComponentThatRenders.prototype, 'render'); + spy(ErrorBoundary.prototype, 'componentDidCatch'); + spy(ErrorBoundary.prototype, 'render'); + spy(App.prototype, 'componentDidCatch'); + spy(App.prototype, 'render'); + + render(); + + // ComponentThatThrows + expect(ComponentThatThrows.prototype.componentDidCatch) + .to.not.have.been.called; + + expect(ComponentThatThrows.prototype.componentWillMount) + .to.have.been.calledOnce + .and.to.throw(Error, ERROR_MESSAGE); + + expect(ComponentThatThrows.prototype.render) + .to.not.have.been.called; + + // ComponentThatRenders + expect(ComponentThatRenders.prototype.componentDidCatch) + .to.not.have.been.called; + + expect(ComponentThatRenders.prototype.render) + .to.have.been.calledOnce + .and.to.not.throw(); + + // ErrorBoundary + expect(ErrorBoundary.prototype.componentDidCatch) + .to.have.been.calledOnce + .and.calledWithExactly(THE_ERROR); + + expect(ErrorBoundary.prototype.render) + .to.have.been.calledTwice; + + // App + expect(App.prototype.render) + .to.have.been.calledOnce; + + expect(App.prototype.componentDidCatch) + .to.not.have.been.called; + }); + + it('should invoke ErrorBoundary\'s componentDidCatch from an error thrown in ComponentThatThrows\' render', () => { + const ERROR_MESSAGE = 'render error', + THE_ERROR = new Error(ERROR_MESSAGE); + class ComponentThatThrows extends Component { + componentDidCatch(error) {} + render(props) { + throw THE_ERROR; + return
; // eslint-disable-line + } + } + class ComponentThatRenders extends Component { + componentDidCatch(error) {} + render(props) { + return
; + } + } + class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { throwError: true }; + } + componentDidCatch(error) { + this.setState({ throwError: false }); + } + render(props, { throwError }) { + return throwError + ? + : ; + } + } + class App extends Component { + componentDidCatch(error) {} + render(props) { + return ; + } + } + + spy(ComponentThatThrows.prototype, 'componentDidCatch'); + spy(ComponentThatThrows.prototype, 'render'); + spy(ComponentThatRenders.prototype, 'componentDidCatch'); + spy(ComponentThatRenders.prototype, 'render'); + spy(ErrorBoundary.prototype, 'componentDidCatch'); + spy(ErrorBoundary.prototype, 'render'); + spy(App.prototype, 'componentDidCatch'); + spy(App.prototype, 'render'); + + render(); + + // ComponentThatThrows + expect(ComponentThatThrows.prototype.componentDidCatch) + .to.not.have.been.called; + + expect(ComponentThatThrows.prototype.render) + .to.have.been.calledOnce + .and.to.throw(Error, ERROR_MESSAGE); + + // ComponentThatRenders + expect(ComponentThatRenders.prototype.componentDidCatch) + .to.not.have.been.called; + + expect(ComponentThatRenders.prototype.render) + .to.have.been.calledOnce + .and.to.not.throw(); + + // ErrorBoundary + expect(ErrorBoundary.prototype.componentDidCatch) + .to.have.been.calledOnce + .and.calledWithExactly(THE_ERROR); + + expect(ErrorBoundary.prototype.render) + .to.have.been.calledTwice; + + // App + expect(App.prototype.render) + .to.have.been.calledOnce; + + expect(App.prototype.componentDidCatch) + .to.not.have.been.called; + }); + }); });