Skip to content

Commit 2f486f1

Browse files
committed
Error boundaries.
1 parent 36dfe62 commit 2f486f1

4 files changed

Lines changed: 168 additions & 10 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* Copyright 2013-2015, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @emails react-core
10+
*/
11+
12+
'use strict';
13+
14+
var React;
15+
var ReactDOM;
16+
17+
describe('ReactErrorBoundaries', function() {
18+
19+
beforeEach(function() {
20+
ReactDOM = require('ReactDOM');
21+
React = require('React');
22+
});
23+
24+
it('catches errors from children', function() {
25+
var log = [];
26+
27+
class Box extends React.Component {
28+
constructor(props) {
29+
super(props);
30+
this._errorMessage = undefined;
31+
}
32+
render() {
33+
if (this._errorMessage) {
34+
log.push('Box renderError');
35+
return <div>Error: {this._errorMessage}</div>;
36+
}
37+
log.push('Box render');
38+
var ref = function(x) {
39+
log.push('Inquisitive ref ' + x);
40+
};
41+
return (
42+
<div>
43+
<Inquisitive ref={ref} />
44+
<Angry />
45+
</div>
46+
);
47+
}
48+
handleError(e) {
49+
this._errorMessage = e.message;
50+
}
51+
componentDidMount() {
52+
log.push('Box componentDidMount');
53+
}
54+
componentWillUnmount() {
55+
log.push('Box componentWillUnmount');
56+
}
57+
}
58+
59+
class Inquisitive extends React.Component {
60+
render() {
61+
log.push('Inquisitive render');
62+
return <div>What is love?</div>;
63+
}
64+
componentDidMount() {
65+
log.push('Inquisitive componentDidMount');
66+
}
67+
componentWillUnmount() {
68+
log.push('Inquisitive componentWillUnmount');
69+
}
70+
}
71+
72+
class Angry extends React.Component {
73+
render() {
74+
log.push('Angry render');
75+
throw new Error('Please, do not render me.');
76+
}
77+
componentDidMount() {
78+
log.push('Angry componentDidMount');
79+
}
80+
componentWillUnmount() {
81+
log.push('Angry componentWillUnmount');
82+
}
83+
}
84+
85+
var container = document.createElement('div');
86+
ReactDOM.render(<Box />, container);
87+
expect(container.textContent).toBe('Error: Please, do not render me.');
88+
expect(log).toEqual([
89+
'Box render',
90+
'Inquisitive render',
91+
'Angry render',
92+
'Inquisitive ref null',
93+
'Inquisitive componentWillUnmount',
94+
'Angry componentWillUnmount',
95+
'Box renderError',
96+
]);
97+
});
98+
});

src/renderers/dom/client/ReactReconcileTransaction.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,19 @@ var Mixin = {
137137
return this.reactMountReady;
138138
},
139139

140+
/**
141+
* Save current transaction state -- if the return value from this method is
142+
* passed to `rollback`, the transaction will be reset to that state.
143+
*/
144+
checkpoint: function() {
145+
// reactMountReady is the our only stateful wrapper
146+
return this.reactMountReady.checkpoint();
147+
},
148+
149+
rollback: function(checkpoint) {
150+
this.reactMountReady.rollback(checkpoint);
151+
},
152+
140153
/**
141154
* `PooledClass` looks for this, and will invoke this before allowing this
142155
* instance to be reused.

src/renderers/shared/reconciler/ReactCompositeComponent.js

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ var invariant = require('invariant');
2828
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
2929
var warning = require('warning');
3030

31+
/**
32+
* Used to indicate that no error has been thrown (since you can actually throw
33+
* null, undefined, and seemingly anything else).
34+
*/
35+
var NO_ERROR = {};
36+
3137
function getDeclarationErrorAddendum(component) {
3238
var owner = component._currentElement._owner || null;
3339
if (owner) {
@@ -99,16 +105,16 @@ var ReactCompositeComponentMixin = {
99105
this._instance = null;
100106
this._nativeParent = null;
101107
this._nativeContainerInfo = null;
108+
this._renderedNodeType = null;
102109

103110
// See ReactUpdateQueue
104111
this._pendingElement = null;
105112
this._pendingStateQueue = null;
106113
this._pendingReplaceState = false;
107114
this._pendingForceUpdate = false;
108115

109-
this._renderedNodeType = null;
116+
this._caughtError = NO_ERROR;
110117
this._renderedComponent = null;
111-
112118
this._context = null;
113119
this._mountOrder = 0;
114120
this._topLevelWrapper = null;
@@ -278,6 +284,36 @@ var ReactCompositeComponentMixin = {
278284
this._pendingReplaceState = false;
279285
this._pendingForceUpdate = false;
280286

287+
var markup;
288+
if (inst.handleError) {
289+
var checkpoint = transaction.checkpoint();
290+
try {
291+
markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
292+
} catch (e) {
293+
this._caughtError = e;
294+
inst.handleError(e);
295+
this._renderedComponent.unmountComponent();
296+
transaction.rollback(checkpoint);
297+
298+
// Try again - we've informed the component about the error, so they can render an error message this time.
299+
// If this throws again, the error will bubble up (and can be caught by a higher error boundary).
300+
markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
301+
// Don't call componentDidMount (TODO: wait, what? why not?)
302+
return markup;
303+
}
304+
} else {
305+
markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
306+
}
307+
308+
if (inst.componentDidMount) {
309+
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
310+
}
311+
312+
return markup;
313+
},
314+
315+
performInitialMount: function(renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
316+
var inst = this._instance;
281317
if (inst.componentWillMount) {
282318
inst.componentWillMount();
283319
// When mounting, calls to `setState` by `componentWillMount` will set
@@ -304,9 +340,6 @@ var ReactCompositeComponentMixin = {
304340
nativeContainerInfo,
305341
this._processChildContext(context)
306342
);
307-
if (inst.componentDidMount) {
308-
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
309-
}
310343

311344
return markup;
312345
},
@@ -328,10 +361,12 @@ var ReactCompositeComponentMixin = {
328361
inst.componentWillUnmount();
329362
}
330363

331-
ReactReconciler.unmountComponent(this._renderedComponent);
332-
this._renderedNodeType = null;
333-
this._renderedComponent = null;
334-
this._instance = null;
364+
if (this._renderedComponent) {
365+
ReactReconciler.unmountComponent(this._renderedComponent);
366+
this._renderedNodeType = null;
367+
this._renderedComponent = null;
368+
this._instance = null;
369+
}
335370

336371
// Reset pending fields
337372
// Even if this component is scheduled for another update in ReactUpdates,
@@ -786,7 +821,8 @@ var ReactCompositeComponentMixin = {
786821
*/
787822
_renderValidatedComponentWithoutOwnerOrContext: function() {
788823
var inst = this._instance;
789-
var renderedComponent = inst.render();
824+
var renderedComponent;
825+
renderedComponent = inst.render();
790826
if (__DEV__) {
791827
// We allow auto-mocks to proceed as if they're returning null.
792828
if (typeof renderedComponent === 'undefined' &&

src/shared/utils/CallbackQueue.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ assign(CallbackQueue.prototype, {
7272
}
7373
},
7474

75+
checkpoint: function() {
76+
return this._callbacks ? this._callbacks.length : 0;
77+
},
78+
79+
rollback: function(len) {
80+
if (this._callbacks) {
81+
this._callbacks.length = len;
82+
this._contexts.length = len;
83+
}
84+
},
85+
7586
/**
7687
* Resets the internal queue.
7788
*

0 commit comments

Comments
 (0)