@samitouri / QOS-React-2 / commits / 3afced6a75

Error boundaries.

Jim committed Apr 8, 2015 at 18:58 UTC 3afced6a75382ddf471a5490491b869a5f964689
4 files changed +240 -8
src/core/__tests__/ReactErrorBoundaries-test.js new
+158
@@ -0,0 +1,158 @@
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('does not register event handlers for unmounted children', function() {
25 + class Angry extends React.Component {
26 + render() {
27 + throw new Error('Please, do not render me.');
28 + }
29 + }
30 +
31 + class Boundary extends React.Component {
32 + constructor(props) {
33 + super(props);
34 + this.state = {error: false};
35 + }
36 + render() {
37 + if (!this.state.error) {
38 + return (<div><button onClick={this.onClick}>ClickMe</button><Angry /></div>);
39 + } else {
40 + return (<div>Happy Birthday!</div>);
41 + }
42 + }
43 + onClick() {
44 + /* do nothing */
45 + }
46 + unstable_handleError() {
47 + this.setState({error: true});
48 + }
49 + }
50 +
51 + var EventPluginHub = require('EventPluginHub');
52 + var container = document.createElement('div');
53 + EventPluginHub.putListener = jest.genMockFn();
54 + ReactDOM.render(<Boundary />, container);
55 + expect(EventPluginHub.putListener).not.toBeCalled();
56 + });
57 +
58 + it('expect uneventful render to succeed', function() {
59 + class Boundary extends React.Component {
60 + constructor(props) {
61 + super(props);
62 + this.state = {error: false};
63 + }
64 + render() {
65 + return (<div><button onClick={this.onClick}>ClickMe</button></div>);
66 + }
67 + onClick() {
68 + /* do nothing */
69 + }
70 + unstable_handleError() {
71 + this.setState({error: true});
72 + }
73 + }
74 +
75 + var EventPluginHub = require('EventPluginHub');
76 + var container = document.createElement('div');
77 + EventPluginHub.putListener = jest.genMockFn();
78 + ReactDOM.render(<Boundary />, container);
79 + expect(EventPluginHub.putListener).toBeCalled();
80 + });
81 +
82 +
83 + it('catches errors from children', function() {
84 + var log = [];
85 +
86 + class Box extends React.Component {
87 + constructor(props) {
88 + super(props);
89 + this.state = {errorMessage: null};
90 + }
91 + render() {
92 + if (this.state.errorMessage != null) {
93 + log.push('Box renderError');
94 + return <div>Error: {this.state.errorMessage}</div>;
95 + }
96 + log.push('Box render');
97 + var ref = function(x) {
98 + log.push('Inquisitive ref ' + x);
99 + };
100 + return (
101 + <div>
102 + <Inquisitive ref={ref} />
103 + <Angry />
104 + </div>
105 + );
106 + }
107 + unstable_handleError(e) {
108 + this.setState({errorMessage: e.message});
109 + }
110 + componentDidMount() {
111 + log.push('Box componentDidMount');
112 + }
113 + componentWillUnmount() {
114 + log.push('Box componentWillUnmount');
115 + }
116 + }
117 +
118 + class Inquisitive extends React.Component {
119 + render() {
120 + log.push('Inquisitive render');
121 + return <div>What is love?</div>;
122 + }
123 + componentDidMount() {
124 + log.push('Inquisitive componentDidMount');
125 + }
126 + componentWillUnmount() {
127 + log.push('Inquisitive componentWillUnmount');
128 + }
129 + }
130 +
131 + class Angry extends React.Component {
132 + render() {
133 + log.push('Angry render');
134 + throw new Error('Please, do not render me.');
135 + }
136 + componentDidMount() {
137 + log.push('Angry componentDidMount');
138 + }
139 + componentWillUnmount() {
140 + log.push('Angry componentWillUnmount');
141 + }
142 + }
143 +
144 + var container = document.createElement('div');
145 + ReactDOM.render(<Box />, container);
146 + expect(container.textContent).toBe('Error: Please, do not render me.');
147 + expect(log).toEqual([
148 + 'Box render',
149 + 'Inquisitive render',
150 + 'Angry render',
151 + 'Inquisitive ref null',
152 + 'Inquisitive componentWillUnmount',
153 + 'Angry componentWillUnmount',
154 + 'Box renderError',
155 + 'Box componentDidMount',
156 + ]);
157 + });
158 +});
src/renderers/dom/client/ReactReconcileTransaction.js
+13
@@ -136,6 +136,19 @@ var Mixin = {
136 return this.reactMountReady;
137 },
138
139 + /**
140 + * Save current transaction state -- if the return value from this method is
141 + * passed to `rollback`, the transaction will be reset to that state.
142 + */
143 + checkpoint: function() {
144 + // reactMountReady is the our only stateful wrapper
145 + return this.reactMountReady.checkpoint();
146 + },
147 +
148 + rollback: function(checkpoint) {
149 + this.reactMountReady.rollback(checkpoint);
150 + },
151 +
152 /**
153 * `PooledClass` looks for this, and will invoke this before allowing this
154 * instance to be reused.
src/renderers/shared/reconciler/ReactCompositeComponent.js
+58 -8
@@ -108,7 +108,6 @@ var ReactCompositeComponentMixin = {
108
109 this._renderedNodeType = null;
110 this._renderedComponent = null;
111 -
111 this._context = null;
112 this._mountOrder = 0;
113 this._topLevelWrapper = null;
@@ -278,6 +277,58 @@ var ReactCompositeComponentMixin = {
277 this._pendingReplaceState = false;
278 this._pendingForceUpdate = false;
279
280 + var markup;
281 + if (inst.unstable_handleError) {
282 + markup = this.performInitialMountWithErrorHandling(
283 + renderedElement,
284 + nativeParent,
285 + nativeContainerInfo,
286 + transaction,
287 + context
288 + );
289 + } else {
290 + markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
291 + }
292 +
293 + if (inst.componentDidMount) {
294 + transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
295 + }
296 +
297 + return markup;
298 + },
299 +
300 + performInitialMountWithErrorHandling: function(
301 + renderedElement,
302 + nativeParent,
303 + nativeContainerInfo,
304 + transaction,
305 + context
306 + ) {
307 + var markup;
308 + var checkpoint = transaction.checkpoint();
309 + try {
310 + markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
311 + } catch (e) {
312 + // Roll back to checkpoint, handle error (which may add items to the transaction), and take a new checkpoint
313 + transaction.rollback(checkpoint);
314 + this._instance.unstable_handleError(e);
315 + if (this._pendingStateQueue) {
316 + this._instance.state = this._processPendingState(this._instance.props, this._instance.context);
317 + }
318 + checkpoint = transaction.checkpoint();
319 +
320 + this._renderedComponent.unmountComponent();
321 + transaction.rollback(checkpoint);
322 +
323 + // Try again - we've informed the component about the error, so they can render an error message this time.
324 + // If this throws again, the error will bubble up (and can be caught by a higher error boundary).
325 + markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
326 + }
327 + return markup;
328 + },
329 +
330 + performInitialMount: function(renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
331 + var inst = this._instance;
332 if (inst.componentWillMount) {
333 inst.componentWillMount();
334 // When mounting, calls to `setState` by `componentWillMount` will set
@@ -304,9 +355,6 @@ var ReactCompositeComponentMixin = {
355 nativeContainerInfo,
356 this._processChildContext(context)
357 );
307 - if (inst.componentDidMount) {
308 - transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
309 - }
358
359 return markup;
360 },
@@ -328,10 +376,12 @@ var ReactCompositeComponentMixin = {
376 inst.componentWillUnmount();
377 }
378
331 - ReactReconciler.unmountComponent(this._renderedComponent);
332 - this._renderedNodeType = null;
333 - this._renderedComponent = null;
334 - this._instance = null;
379 + if (this._renderedComponent) {
380 + ReactReconciler.unmountComponent(this._renderedComponent);
381 + this._renderedNodeType = null;
382 + this._renderedComponent = null;
383 + this._instance = null;
384 + }
385
386 // Reset pending fields
387 // Even if this component is scheduled for another update in ReactUpdates,
src/shared/utils/CallbackQueue.js
+11
@@ -72,6 +72,17 @@ assign(CallbackQueue.prototype, {
72 }
73 },
74
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 +
86 /**
87 * Resets the internal queue.
88 *