diff --git a/src/core/__tests__/ReactErrorBoundaries-test.js b/src/core/__tests__/ReactErrorBoundaries-test.js
new file mode 100644
index 0000000000000..6c7333c8293c2
--- /dev/null
+++ b/src/core/__tests__/ReactErrorBoundaries-test.js
@@ -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 (
);
+ } else {
+ return (Happy Birthday!
);
+ }
+ }
+ onClick() {
+ /* do nothing */
+ }
+ unstable_handleError() {
+ this.setState({error: true});
+ }
+ }
+
+ var EventPluginHub = require('EventPluginHub');
+ var container = document.createElement('div');
+ EventPluginHub.putListener = jest.genMockFn();
+ ReactDOM.render( , 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 (ClickMe
);
+ }
+ onClick() {
+ /* do nothing */
+ }
+ unstable_handleError() {
+ this.setState({error: true});
+ }
+ }
+
+ var EventPluginHub = require('EventPluginHub');
+ var container = document.createElement('div');
+ EventPluginHub.putListener = jest.genMockFn();
+ ReactDOM.render( , 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 Error: {this.state.errorMessage}
;
+ }
+ log.push('Box render');
+ var ref = function(x) {
+ log.push('Inquisitive ref ' + x);
+ };
+ return (
+
+ );
+ }
+ 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 What is love?
;
+ }
+ 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( , 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',
+ ]);
+ });
+});
diff --git a/src/renderers/dom/client/ReactReconcileTransaction.js b/src/renderers/dom/client/ReactReconcileTransaction.js
index 3cdebad2d77bd..6cf2c22631bd1 100644
--- a/src/renderers/dom/client/ReactReconcileTransaction.js
+++ b/src/renderers/dom/client/ReactReconcileTransaction.js
@@ -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.
diff --git a/src/renderers/shared/reconciler/ReactCompositeComponent.js b/src/renderers/shared/reconciler/ReactCompositeComponent.js
index b341eec2f8d37..13657ad204a61 100644
--- a/src/renderers/shared/reconciler/ReactCompositeComponent.js
+++ b/src/renderers/shared/reconciler/ReactCompositeComponent.js
@@ -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,
diff --git a/src/shared/utils/CallbackQueue.js b/src/shared/utils/CallbackQueue.js
index 089d6802920c0..2fca444c49b2f 100644
--- a/src/shared/utils/CallbackQueue.js
+++ b/src/shared/utils/CallbackQueue.js
@@ -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.
*