Skip to content

Commit

Permalink
Error boundaries.
Browse files Browse the repository at this point in the history
  • Loading branch information
jimfb committed Dec 15, 2015
1 parent 36dfe62 commit 1a1727d
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 8 deletions.
99 changes: 99 additions & 0 deletions src/core/__tests__/ReactErrorBoundaries-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* 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>
);
}
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',
]);
});
});
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
67 changes: 59 additions & 8 deletions src/renderers/shared/reconciler/ReactCompositeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ var ReactCompositeComponentMixin = {

this._renderedNodeType = null;
this._renderedComponent = null;

this._context = null;
this._mountOrder = 0;
this._topLevelWrapper = null;
Expand Down Expand Up @@ -278,6 +277,59 @@ 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 {
var 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
Expand All @@ -304,9 +356,6 @@ var ReactCompositeComponentMixin = {
nativeContainerInfo,
this._processChildContext(context)
);
if (inst.componentDidMount) {
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
}

return markup;
},
Expand All @@ -328,10 +377,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 1a1727d

Please sign in to comment.