mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user