Skip to content

Commit

Permalink
Error boundaries.
Browse files Browse the repository at this point in the history
  • Loading branch information
jimfb committed Dec 11, 2015
1 parent 36dfe62 commit 47b0a55
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 9 deletions.
98 changes: 98 additions & 0 deletions src/core/__tests__/ReactErrorBoundaries-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* 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('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>
);
}
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',
]);
});
});
13 changes: 13 additions & 0 deletions src/renderers/dom/client/ReactReconcileTransaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,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.
Expand Down
61 changes: 52 additions & 9 deletions src/renderers/shared/reconciler/ReactCompositeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ var invariant = require('invariant');
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
var warning = require('warning');

/**
* Used to indicate that no error has been thrown (since you can actually throw
* null, undefined, and seemingly anything else).
*/
var NO_ERROR = {};

function getDeclarationErrorAddendum(component) {
var owner = component._currentElement._owner || null;
if (owner) {
Expand Down Expand Up @@ -99,16 +105,16 @@ var ReactCompositeComponentMixin = {
this._instance = null;
this._nativeParent = null;
this._nativeContainerInfo = null;
this._renderedNodeType = null;

// See ReactUpdateQueue
this._pendingElement = null;
this._pendingStateQueue = null;
this._pendingReplaceState = false;
this._pendingForceUpdate = false;

this._renderedNodeType = null;
this._caughtError = NO_ERROR;
this._renderedComponent = null;

this._context = null;
this._mountOrder = 0;
this._topLevelWrapper = null;
Expand Down Expand Up @@ -278,6 +284,44 @@ var ReactCompositeComponentMixin = {
this._pendingReplaceState = false;
this._pendingForceUpdate = false;

var markup;
if (inst.handleError) {
var checkpoint = transaction.checkpoint();
try {
markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
} catch (e) {
this._caughtError = e;

// Roll back to checkpoint, handle error (which may add items to the transaction), and take a new checkpoint
transaction.rollback(checkpoint);
inst.handleError(e);
if (this._pendingStateQueue) {
inst.state = this._processPendingState(inst.props, inst.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);
// Don't call componentDidMount (TODO: wait, what? why not?)
return markup;
}
} else {
markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
}

if (inst.componentDidMount) {
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
}

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
Expand All @@ -304,9 +348,6 @@ var ReactCompositeComponentMixin = {
nativeContainerInfo,
this._processChildContext(context)
);
if (inst.componentDidMount) {
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
}

return markup;
},
Expand All @@ -328,10 +369,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,
Expand Down
11 changes: 11 additions & 0 deletions src/shared/utils/CallbackQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down

0 comments on commit 47b0a55

Please sign in to comment.