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

Adds support for componentDidCatch() #66

Closed
wants to merge 8 commits into from
17 changes: 14 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ function renderToString(vnode, context, opts, inner, isSvgMode) {
nodeName = getComponentName(nodeName);
}
else {
let props = getNodeProps(vnode),
let c,
props = getNodeProps(vnode),
rendered;

if (!nodeName.prototype || typeof nodeName.prototype.render!=='function') {
Expand All @@ -68,7 +69,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._disable = c.__x = true;
c.props = props;
Expand All @@ -82,7 +83,17 @@ function renderToString(vnode, context, opts, inner, isSvgMode) {
}
}

return renderToString(rendered, context, opts, opts.shallowHighOrder!==false);
Copy link
Member

Choose a reason for hiding this comment

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

Idea: we can circumvent having to catch and re-throw the error when there's no componentDidCatch:

if (c && c.componentDidCatch) {
	try {
		return renderToString(rendered, context, opts, opts.shallowHighOrder!==false);
	}
	catch (error) {
		c.componentDidCatch(error);
		rendered = c.render(c.props, c.state, c.context);
		// now we fall through to the non-componentDidCatch render!
	}
}
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;
}
}
}

Expand Down
254 changes: 254 additions & 0 deletions test/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -610,4 +610,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 <div {...props} />;
}
}
class ComponentThatRenders extends Component {
componentDidCatch(error) {}
render(props) {
return <div {...props} />;
}
}
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { throwError: true };
}
componentDidCatch(error) {
this.setState({ throwError: false });
}
render(props, { throwError }) {
return throwError
? <ComponentThatThrows />
: <ComponentThatRenders />;
}
}
class App extends Component {
componentDidCatch(error) {}
render(props) {
return <ErrorBoundary />;
}
}

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(<App />);

// 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 <div {...props} />;
}
}
class ComponentThatRenders extends Component {
componentDidCatch(error) {}
render(props) {
return <div {...props} />;
}
}
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { throwError: true };
}
componentDidCatch(error) {
this.setState({ throwError: false });
}
render(props, { throwError }) {
return throwError
? <ComponentThatThrows />
: <ComponentThatRenders />;
}
}
class App extends Component {
componentDidCatch(error) {}
render(props) {
return <ErrorBoundary />;
}
}

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(<App />);

// 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 <div {...props} />; // eslint-disable-line
}
}
class ComponentThatRenders extends Component {
componentDidCatch(error) {}
render(props) {
return <div {...props} />;
}
}
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { throwError: true };
}
componentDidCatch(error) {
this.setState({ throwError: false });
}
render(props, { throwError }) {
return throwError
? <ComponentThatThrows />
: <ComponentThatRenders />;
}
}
class App extends Component {
componentDidCatch(error) {}
render(props) {
return <ErrorBoundary />;
}
}

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(<App />);

// 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;
});
});
});