Merge pull request #5602 from jimfb/error-boundaries

Error boundaries
This commit is contained in:
Jim
2015-12-16 13:04:45 -08:00
4 changed files with 240 additions and 8 deletions
@@ -0,0 +1,158 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
'use strict';
var React;
var ReactDOM;
describe('ReactErrorBoundaries', function() {
beforeEach(function() {
ReactDOM = require('ReactDOM');
React = require('React');
});
it('does not register event handlers for unmounted children', function() {
class Angry extends React.Component {
render() {
throw new Error('Please, do not render me.');
}
}
class Boundary extends React.Component {
constructor(props) {
super(props);
this.state = {error: false};
}
render() {
if (!this.state.error) {
return (<div><button onClick={this.onClick}>ClickMe</button><Angry /></div>);
} else {
return (<div>Happy Birthday!</div>);
}
}
onClick() {
/* do nothing */
}
unstable_handleError() {
this.setState({error: true});
}
}
var EventPluginHub = require('EventPluginHub');
var container = document.createElement('div');
EventPluginHub.putListener = jest.genMockFn();
ReactDOM.render(<Boundary />, container);
expect(EventPluginHub.putListener).not.toBeCalled();
});
it('expect uneventful render to succeed', function() {
class Boundary extends React.Component {
constructor(props) {
super(props);
this.state = {error: false};
}
render() {
return (<div><button onClick={this.onClick}>ClickMe</button></div>);
}
onClick() {
/* do nothing */
}
unstable_handleError() {
this.setState({error: true});
}
}
var EventPluginHub = require('EventPluginHub');
var container = document.createElement('div');
EventPluginHub.putListener = jest.genMockFn();
ReactDOM.render(<Boundary />, container);
expect(EventPluginHub.putListener).toBeCalled();
});
it('catches errors from children', function() {
var log = [];
class Box extends React.Component {
constructor(props) {
super(props);
this.state = {errorMessage: null};
}
render() {
if (this.state.errorMessage != null) {
log.push('Box renderError');
return <div>Error: {this.state.errorMessage}</div>;
}
log.push('Box render');
var ref = function(x) {
log.push('Inquisitive ref ' + x);
};
return (
<div>
<Inquisitive ref={ref} />
<Angry />
</div>
);
}
unstable_handleError(e) {
this.setState({errorMessage: e.message});
}
componentDidMount() {
log.push('Box componentDidMount');
}
componentWillUnmount() {
log.push('Box componentWillUnmount');
}
}
class Inquisitive extends React.Component {
render() {
log.push('Inquisitive render');
return <div>What is love?</div>;
}
componentDidMount() {
log.push('Inquisitive componentDidMount');
}
componentWillUnmount() {
log.push('Inquisitive componentWillUnmount');
}
}
class Angry extends React.Component {
render() {
log.push('Angry render');
throw new Error('Please, do not render me.');
}
componentDidMount() {
log.push('Angry componentDidMount');
}
componentWillUnmount() {
log.push('Angry componentWillUnmount');
}
}
var container = document.createElement('div');
ReactDOM.render(<Box />, container);
expect(container.textContent).toBe('Error: Please, do not render me.');
expect(log).toEqual([
'Box render',
'Inquisitive render',
'Angry render',
'Inquisitive ref null',
'Inquisitive componentWillUnmount',
'Angry componentWillUnmount',
'Box renderError',
'Box componentDidMount',
]);
});
});
@@ -136,6 +136,19 @@ var Mixin = {
return this.reactMountReady;
},
/**
* Save current transaction state -- if the return value from this method is
* passed to `rollback`, the transaction will be reset to that state.
*/
checkpoint: function() {
// reactMountReady is the our only stateful wrapper
return this.reactMountReady.checkpoint();
},
rollback: function(checkpoint) {
this.reactMountReady.rollback(checkpoint);
},
/**
* `PooledClass` looks for this, and will invoke this before allowing this
* instance to be reused.
@@ -108,7 +108,6 @@ var ReactCompositeComponentMixin = {
this._renderedNodeType = null;
this._renderedComponent = null;
this._context = null;
this._mountOrder = 0;
this._topLevelWrapper = null;
@@ -278,6 +277,58 @@ var ReactCompositeComponentMixin = {
this._pendingReplaceState = false;
this._pendingForceUpdate = false;
var markup;
if (inst.unstable_handleError) {
markup = this.performInitialMountWithErrorHandling(
renderedElement,
nativeParent,
nativeContainerInfo,
transaction,
context
);
} else {
markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
}
if (inst.componentDidMount) {
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
}
return markup;
},
performInitialMountWithErrorHandling: function(
renderedElement,
nativeParent,
nativeContainerInfo,
transaction,
context
) {
var markup;
var checkpoint = transaction.checkpoint();
try {
markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
} catch (e) {
// Roll back to checkpoint, handle error (which may add items to the transaction), and take a new checkpoint
transaction.rollback(checkpoint);
this._instance.unstable_handleError(e);
if (this._pendingStateQueue) {
this._instance.state = this._processPendingState(this._instance.props, this._instance.context);
}
checkpoint = transaction.checkpoint();
this._renderedComponent.unmountComponent();
transaction.rollback(checkpoint);
// Try again - we've informed the component about the error, so they can render an error message this time.
// If this throws again, the error will bubble up (and can be caught by a higher error boundary).
markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
}
return markup;
},
performInitialMount: function(renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
var inst = this._instance;
if (inst.componentWillMount) {
inst.componentWillMount();
// When mounting, calls to `setState` by `componentWillMount` will set
@@ -304,9 +355,6 @@ var ReactCompositeComponentMixin = {
nativeContainerInfo,
this._processChildContext(context)
);
if (inst.componentDidMount) {
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
}
return markup;
},
@@ -328,10 +376,12 @@ var ReactCompositeComponentMixin = {
inst.componentWillUnmount();
}
ReactReconciler.unmountComponent(this._renderedComponent);
this._renderedNodeType = null;
this._renderedComponent = null;
this._instance = null;
if (this._renderedComponent) {
ReactReconciler.unmountComponent(this._renderedComponent);
this._renderedNodeType = null;
this._renderedComponent = null;
this._instance = null;
}
// Reset pending fields
// Even if this component is scheduled for another update in ReactUpdates,
+11
View File
@@ -72,6 +72,17 @@ assign(CallbackQueue.prototype, {
}
},
checkpoint: function() {
return this._callbacks ? this._callbacks.length : 0;
},
rollback: function(len) {
if (this._callbacks) {
this._callbacks.length = len;
this._contexts.length = len;
}
},
/**
* Resets the internal queue.
*