Skip to content

Commit

Permalink
Initial pass at the easy case of updates (updates that start at the r…
Browse files Browse the repository at this point in the history
…oot).
  • Loading branch information
jim committed Feb 26, 2016
1 parent 1387188 commit d959599
Show file tree
Hide file tree
Showing 2 changed files with 253 additions and 7 deletions.
205 changes: 204 additions & 1 deletion src/core/__tests__/ReactErrorBoundaries-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('ReactErrorBoundaries', function() {
expect(EventPluginHub.putListener).not.toBeCalled();
});

it('will catch exceptions in componentWillUnmount', function() {
it('will catch exceptions in componentWillUnmount initial render', function() {
class ErrorBoundary extends React.Component {
constructor() {
super();
Expand Down Expand Up @@ -236,4 +236,207 @@ describe('ReactErrorBoundaries', function() {
'Box componentWillUnmount',
]);
});

it('catches errors on update', 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} />
{this.props.angry ? <Angry /> : <div />}
</div>
);
}
unstable_handleError(e) {
log.push('error handled');
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 angry={false} />, container);
ReactDOM.render(<Box angry={true} />, container);
expect(container.textContent).toBe('Error: Please, do not render me.');
expect(log).toEqual([
'Box render',
'Inquisitive render',
'Inquisitive componentDidMount',
'Inquisitive ref [object Object]',
'Box componentDidMount',
'Box render',
'Inquisitive ref null',
'Inquisitive render',
'Angry render',
'error handled',
'Inquisitive ref null',
'Inquisitive componentWillUnmount',
'Box renderError',
]);
});

it('catches componentWillUnmount errors on update', function() {
var log = [];

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {errorMessage: null};
}
render() {
if (this.state.errorMessage != null) {
log.push('Box renderError');
return <div>Error: I am now a sad component :(</div>;
}
log.push('Box render');

return (
<div>
<BrokenUnmount />
<BrokenUnmount />
{this.props.angry ? null : <BrokenUnmount />}
</div>
);
}
unstable_handleError(e) {
log.push('error handled');
this.setState({errorMessage: e.message});
}
componentDidMount() {
log.push('Box componentDidMount');
}
componentWillUnmount() {
log.push('Box componentWillUnmount');
}
}

class BrokenUnmount extends React.Component {
render() {
return <div />;
}
componentWillUnmount() {
log.push('BrokenUnmount is attempting to unmount');
throw new Error('Always broken.');
}
}

var container = document.createElement('div');
ReactDOM.render(<ErrorBoundary angry={false} />, container);
ReactDOM.render(<ErrorBoundary angry={true} />, container);
expect(container.textContent).toBe('Error: I am now a sad component :(');
expect(log).toEqual([
'Box render',
'Box componentDidMount',
'Box render',
'BrokenUnmount is attempting to unmount',
'error handled',
'BrokenUnmount is attempting to unmount',
'BrokenUnmount is attempting to unmount',
'BrokenUnmount is attempting to unmount',
'Box renderError',
]);
ReactDOM.unmountComponentAtNode(container);
expect(log).toEqual([
'Box render',
'Box componentDidMount',
'Box render',
'BrokenUnmount is attempting to unmount',
'error handled',
'BrokenUnmount is attempting to unmount',
'BrokenUnmount is attempting to unmount',
'BrokenUnmount is attempting to unmount',
'Box renderError',
'Box componentWillUnmount',
]);
});

it('catches componentWillUnmount errors nested children', function() {
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {errorMessage: null};
}
render() {
if (this.state.errorMessage != null) {
return <div>Error: I am now a sad component :(</div>;
}

return (
<div>
<InnocentParent />
{this.props.angry ? null : <InnocentParent />}
</div>
);
}
unstable_handleError(e) {
this.setState({errorMessage: e.message});
}
}

class InnocentParent extends React.Component {
render() {
return <BrokenUnmount />;
}
}

class BrokenUnmount extends React.Component {
render() {
return <div />;
}
componentWillUnmount() {
throw new Error('Always broken.');
}
}

var container = document.createElement('div');
ReactDOM.render(<ErrorBoundary angry={false} />, container);
ReactDOM.render(<ErrorBoundary angry={true} />, container);
expect(container.textContent).toBe('Error: I am now a sad component :(');
ReactDOM.unmountComponentAtNode(container);
});
});
55 changes: 49 additions & 6 deletions src/renderers/shared/reconciler/ReactCompositeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,6 @@ var ReactCompositeComponentMixin = {
this._instance.state = this._processPendingState(this._instance.props, this._instance.context);
}
checkpoint = transaction.checkpoint();

this._renderedComponent.unmountComponent(true);
transaction.rollback(checkpoint);

Expand Down Expand Up @@ -406,7 +405,7 @@ var ReactCompositeComponentMixin = {
var name = this.getName() + '.componentWillUnmount()';
ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
} else {
inst.componentWillUnmount();
inst.componentWillUnmount(safely);
}
}

Expand Down Expand Up @@ -808,7 +807,11 @@ var ReactCompositeComponentMixin = {
inst.state = nextState;
inst.context = nextContext;

this._updateRenderedComponent(transaction, unmaskedContext);
if (inst.unstable_handleError) {
this._updateRenderedComponentWithErrorHandling(transaction, unmaskedContext);
} else {
this._updateRenderedComponent(transaction, unmaskedContext);
}

if (hasComponentDidUpdate) {
transaction.getReactMountReady().enqueue(
Expand All @@ -818,26 +821,66 @@ var ReactCompositeComponentMixin = {
}
},

/**
* Call the component's `render` method and update the DOM accordingly.
*
* @param {ReactReconcileTransaction} transaction
* @internal
*/
_updateRenderedComponentWithErrorHandling: function(transaction, context) {
var checkpoint = transaction.checkpoint();
try {
this._updateRenderedComponent(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();

// Gracefully update to a clean state
this._updateRenderedComponentWithNextElement(transaction, context, null, true);

// 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).
this._updateRenderedComponent(transaction, context);
}
},

/**
* Call the component's `render` method and update the DOM accordingly.
*
* @param {ReactReconcileTransaction} transaction
* @internal
*/
_updateRenderedComponent: function(transaction, context) {
var nextRenderedElement = this._renderValidatedComponent();
this._updateRenderedComponentWithNextElement(transaction, context, nextRenderedElement, false);
},

/**
* Call the component's `render` method and update the DOM accordingly.
*
* @param {ReactReconcileTransaction} transaction
* @internal
*/
_updateRenderedComponentWithNextElement: function(transaction, context, nextRenderedElement, safely) {
var prevComponentInstance = this._renderedComponent;
var prevRenderedElement = prevComponentInstance._currentElement;
var nextRenderedElement = this._renderValidatedComponent();
if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
ReactReconciler.receiveComponent(
prevComponentInstance,
nextRenderedElement,
transaction,
this._processChildContext(context)
this._processChildContext(context),
safely
);
} else {
var oldNativeNode = ReactReconciler.getNativeNode(prevComponentInstance);
ReactReconciler.unmountComponent(prevComponentInstance, false);
ReactReconciler.unmountComponent(prevComponentInstance, safely);

this._renderedNodeType = ReactNodeTypes.getType(nextRenderedElement);
this._renderedComponent = this._instantiateReactComponent(
Expand Down

0 comments on commit d959599

Please sign in to comment.