[Fiber] Error Boundaries (#8095)
Dan Abramov committed
Oct 28, 2016 at 10:29 UTC
3c6abbfff7c0bf0e46da714e3e38fa04aca8bba2
4 files changed
+631
-197
src/renderers/shared/fiber/ReactFiberCommitWork.js
+86
-23
@@ -12,6 +12,7 @@
12
13
'use strict';
14
15
+import type { TrappedError } from 'ReactFiberErrorBoundary';
16
import type { Fiber } from 'ReactFiber';
17
import type { FiberRoot } from 'ReactFiberRoot';
18
import type { HostConfig } from 'ReactFiberReconciler';
@@ -23,6 +24,7 @@ var {
24
HostComponent,
25
HostText,
26
} = ReactTypeOfWork;
27
+var { trapError } = require('ReactFiberErrorBoundary');
28
var { callCallbacks } = require('ReactFiberUpdateQueue');
29
30
var {
@@ -155,7 +157,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
157
}
158
}
159
158
- function commitNestedUnmounts(root : Fiber) {
160
+ function commitNestedUnmounts(root : Fiber): Array<TrappedError> | null {
161
+ // Since errors are rare, we allocate this array on demand.
162
+ let trappedErrors = null;
163
+
164
// While we're inside a removed host node we don't want to call
165
// removeChild on the inner nodes because they're removed by the top
166
// call anyway. We also want to call componentWillUnmount on all
@@ -163,39 +168,58 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
168
// we do an inner loop while we're still inside the host node.
169
let node : Fiber = root;
170
while (true) {
166
- commitUnmount(node);
171
+ const error = commitUnmount(node);
172
+ if (error) {
173
+ trappedErrors = trappedErrors || [];
174
+ trappedErrors.push(error);
175
+ }
176
if (node.child) {
177
// TODO: Coroutines need to visit the stateNode.
178
node = node.child;
179
continue;
180
}
181
if (node === root) {
173
- return;
182
+ return trappedErrors;
183
}
184
while (!node.sibling) {
185
if (!node.return || node.return === root) {
177
- return;
186
+ return trappedErrors;
187
}
188
node = node.return;
189
}
190
node = node.sibling;
191
}
192
+ return trappedErrors;
193
}
194
185
- function unmountHostComponents(parent, current) {
195
+ function unmountHostComponents(parent, current): Array<TrappedError> | null {
196
+ // Since errors are rare, we allocate this array on demand.
197
+ let trappedErrors = null;
198
+
199
// We only have the top Fiber that was inserted but we need recurse down its
200
// children to find all the terminal nodes.
201
let node : Fiber = current;
202
while (true) {
203
if (node.tag === HostComponent || node.tag === HostText) {
191
- commitNestedUnmounts(node);
204
+ const errors = commitNestedUnmounts(node);
205
+ if (errors) {
206
+ if (!trappedErrors) {
207
+ trappedErrors = errors;
208
+ } else {
209
+ trappedErrors.push.apply(trappedErrors, errors);
210
+ }
211
+ }
212
// After all the children have unmounted, it is now safe to remove the
213
// node from the tree.
214
if (parent) {
215
removeChild(parent, node.stateNode);
216
}
217
} else {
198
- commitUnmount(node);
218
+ const error = commitUnmount(node);
219
+ if (error) {
220
+ trappedErrors = trappedErrors || [];
221
+ trappedErrors.push(error);
222
+ }
223
if (node.child) {
224
// TODO: Coroutines need to visit the stateNode.
225
node = node.child;
@@ -203,24 +227,24 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
227
}
228
}
229
if (node === current) {
206
- return;
230
+ return trappedErrors;
231
}
232
while (!node.sibling) {
233
if (!node.return || node.return === current) {
210
- return;
234
+ return trappedErrors;
235
}
236
node = node.return;
237
}
238
node = node.sibling;
239
}
240
+ return trappedErrors;
241
}
242
218
- function commitDeletion(current : Fiber) : void {
243
+ function commitDeletion(current : Fiber) : Array<TrappedError> | null {
244
// Recursively delete all host nodes from the parent.
220
- // TODO: Error handling.
245
const parent = getHostParent(current);
222
-
223
- unmountHostComponents(parent, current);
246
+ // Detach refs and call componentWillUnmount() on the whole subtree.
247
+ const trappedErrors = unmountHostComponents(parent, current);
248
249
// Cut off the return pointers to disconnect it from the tree. Ideally, we
250
// should clear the child pointer of the parent alternate to let this
@@ -233,21 +257,29 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
257
current.alternate.child = null;
258
current.alternate.return = null;
259
}
260
+
261
+ return trappedErrors;
262
}
263
238
- function commitUnmount(current : Fiber) : void {
264
+ function commitUnmount(current : Fiber) : TrappedError | null {
265
switch (current.tag) {
266
case ClassComponent: {
267
detachRef(current);
268
const instance = current.stateNode;
269
if (typeof instance.componentWillUnmount === 'function') {
244
- instance.componentWillUnmount();
270
+ const error = tryCallComponentWillUnmount(instance);
271
+ if (error) {
272
+ return trapError(current, error);
273
+ }
274
}
246
- return;
275
+ return null;
276
}
277
case HostComponent: {
278
detachRef(current);
250
- return;
279
+ return null;
280
+ }
281
+ default: {
282
+ return null;
283
}
284
}
285
}
@@ -292,19 +324,20 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
324
}
325
}
326
295
- function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : void {
327
+ function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : TrappedError | null {
328
switch (finishedWork.tag) {
329
case ClassComponent: {
330
const instance = finishedWork.stateNode;
331
+ let error = null;
332
if (!current) {
333
if (typeof instance.componentDidMount === 'function') {
301
- instance.componentDidMount();
334
+ error = tryCallComponentDidMount(instance);
335
}
336
} else {
337
if (typeof instance.componentDidUpdate === 'function') {
338
const prevProps = current.memoizedProps;
339
const prevState = current.memoizedState;
307
- instance.componentDidUpdate(prevProps, prevState);
340
+ error = tryCallComponentDidUpdate(instance, prevProps, prevState);
341
}
342
}
343
// Clear updates from current fiber. This must go before the callbacks
@@ -320,7 +353,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
353
callCallbacks(callbackList, instance);
354
}
355
attachRef(current, finishedWork, instance);
323
- return;
356
+ if (error) {
357
+ return trapError(finishedWork, error);
358
+ }
359
+ return null;
360
}
361
case HostContainer: {
362
const instance = finishedWork.stateNode;
@@ -333,17 +369,44 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
369
case HostComponent: {
370
const instance : I = finishedWork.stateNode;
371
attachRef(current, finishedWork, instance);
336
- return;
372
+ return null;
373
}
374
case HostText: {
375
// We have no life-cycles associated with text.
340
- return;
376
+ return null;
377
}
378
default:
379
throw new Error('This unit of work tag should not have side-effects.');
380
}
381
}
382
383
+ function tryCallComponentDidMount(instance) {
384
+ try {
385
+ instance.componentDidMount();
386
+ return null;
387
+ } catch (error) {
388
+ return error;
389
+ }
390
+ }
391
+
392
+ function tryCallComponentDidUpdate(instance, prevProps, prevState) {
393
+ try {
394
+ instance.componentDidUpdate(prevProps, prevState);
395
+ return null;
396
+ } catch (error) {
397
+ return error;
398
+ }
399
+ }
400
+
401
+ function tryCallComponentWillUnmount(instance) {
402
+ try {
403
+ instance.componentWillUnmount();
404
+ return null;
405
+ } catch (error) {
406
+ return error;
407
+ }
408
+ }
409
+
410
return {
411
commitInsertion,
412
commitDeletion,
src/renderers/shared/fiber/ReactFiberErrorBoundary.js
new
+53
@@ -0,0 +1,53 @@
1
+/**
2
+ * Copyright 2013-present, 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
+ * @providesModule ReactFiberErrorBoundary
10
+ * @flow
11
+ */
12
+
13
+'use strict';
14
+
15
+import type { Fiber } from 'ReactFiber';
16
+
17
+var {
18
+ ClassComponent,
19
+} = require('ReactTypeOfWork');
20
+
21
+export type TrappedError = {
22
+ boundary: Fiber | null,
23
+ error: any,
24
+};
25
+
26
+function findClosestErrorBoundary(fiber : Fiber): Fiber | null {
27
+ let maybeErrorBoundary = fiber.return;
28
+ while (maybeErrorBoundary) {
29
+ if (maybeErrorBoundary.tag === ClassComponent) {
30
+ const instance = maybeErrorBoundary.stateNode;
31
+ if (typeof instance.unstable_handleError === 'function') {
32
+ return maybeErrorBoundary;
33
+ }
34
+ }
35
+ maybeErrorBoundary = maybeErrorBoundary.return;
36
+ }
37
+ return null;
38
+}
39
+
40
+function trapError(fiber : Fiber, error : any) : TrappedError {
41
+ return {
42
+ boundary: findClosestErrorBoundary(fiber),
43
+ error,
44
+ };
45
+}
46
+
47
+function acknowledgeErrorInBoundary(boundary : Fiber, error : any) {
48
+ const instance = boundary.stateNode;
49
+ instance.unstable_handleError(error);
50
+}
51
+
52
+exports.trapError = trapError;
53
+exports.acknowledgeErrorInBoundary = acknowledgeErrorInBoundary;
src/renderers/shared/fiber/ReactFiberScheduler.js
+134
-14
@@ -23,6 +23,7 @@ var ReactFiberCommitWork = require('ReactFiberCommitWork');
23
var ReactCurrentOwner = require('ReactCurrentOwner');
24
25
var { cloneFiber } = require('ReactFiber');
26
+var { trapError, acknowledgeErrorInBoundary } = require('ReactFiberErrorBoundary');
27
28
var {
29
NoWork,
@@ -109,9 +110,14 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
110
return null;
111
}
112
112
- function commitAllWork(finishedWork : Fiber) {
113
+ function commitAllWork(finishedWork : Fiber, ignoreUnmountingErrors : boolean) {
114
// Commit all the side-effects within a tree.
114
- // TODO: Error handling.
115
+
116
+ // Commit phase is meant to be atomic and non-interruptible.
117
+ // Any errors raised in it should be handled after it is over
118
+ // so that we don't end up in an inconsistent state due to user code.
119
+ // We'll keep track of all caught errors and handle them later.
120
+ let allTrappedErrors = null;
121
122
// First, we'll perform all the host insertions, updates, deletions and
123
// ref unmounts.
@@ -129,7 +135,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
135
commitInsertion(effectfulFiber);
136
const current = effectfulFiber.alternate;
137
commitWork(current, effectfulFiber);
132
- // Clear the effect tag so that we know that this is inserted, before
138
+ // Clear the "placement" from effect tag so that we know that this is inserted, before
139
// any life-cycles like componentDidMount gets called.
140
effectfulFiber.effectTag = Update;
141
break;
@@ -140,7 +146,20 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
146
break;
147
}
148
case Deletion: {
143
- commitDeletion(effectfulFiber);
149
+ // Deletion might cause an error in componentWillUnmount().
150
+ // We will continue nevertheless and handle those later on.
151
+ const trappedErrors = commitDeletion(effectfulFiber);
152
+ // There is a special case where we completely ignore errors.
153
+ // It happens when we already caught an error earlier, and the update
154
+ // is caused by an error boundary trying to render an error message.
155
+ // In this case, we want to blow away the tree without catching errors.
156
+ if (trappedErrors && !ignoreUnmountingErrors) {
157
+ if (!allTrappedErrors) {
158
+ allTrappedErrors = trappedErrors;
159
+ } else {
160
+ allTrappedErrors.push.apply(allTrappedErrors, trappedErrors);
161
+ }
162
+ }
163
break;
164
}
165
}
@@ -155,7 +174,11 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
174
if (effectfulFiber.effectTag === Update ||
175
effectfulFiber.effectTag === PlacementAndUpdate) {
176
const current = effectfulFiber.alternate;
158
- commitLifeCycles(current, effectfulFiber);
177
+ const trappedError = commitLifeCycles(current, effectfulFiber);
178
+ if (trappedError) {
179
+ allTrappedErrors = allTrappedErrors || [];
180
+ allTrappedErrors.push(trappedError);
181
+ }
182
}
183
const next = effectfulFiber.nextEffect;
184
// Ensure that we clean these up so that we don't accidentally keep them.
@@ -173,7 +196,17 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
196
if (finishedWork.effectTag !== NoEffect) {
197
const current = finishedWork.alternate;
198
commitWork(current, finishedWork);
176
- commitLifeCycles(current, finishedWork);
199
+ const trappedError = commitLifeCycles(current, finishedWork);
200
+ if (trappedError) {
201
+ allTrappedErrors = allTrappedErrors || [];
202
+ allTrappedErrors.push(trappedError);
203
+ }
204
+ }
205
+
206
+ // Now that the tree has been committed, we can handle errors.
207
+ if (allTrappedErrors) {
208
+ // TODO: handle multiple errors with distinct boundaries.
209
+ handleError(allTrappedErrors[0]);
210
}
211
}
212
@@ -195,7 +228,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
228
workInProgress.pendingWorkPriority = newPriority;
229
}
230
198
- function completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
231
+ function completeUnitOfWork(workInProgress : Fiber, ignoreUnmountingErrors : boolean) : ?Fiber {
232
while (true) {
233
// The current, flushed, state of this fiber is the alternate.
234
// Ideally nothing should rely on this, but relying on it here
@@ -267,7 +300,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
300
// "next" scheduled work since we've already scanned passed. That
301
// also ensures that work scheduled during reconciliation gets deferred.
302
// const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork;
270
- commitAllWork(workInProgress);
303
+ commitAllWork(workInProgress, ignoreUnmountingErrors);
304
const nextWork = findNextUnitOfWork();
305
// if (!nextWork && hasMoreWork) {
306
// TODO: This can happen when some deep work completes and we don't
@@ -281,7 +314,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
314
}
315
}
316
284
- function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
317
+ function performUnitOfWork(workInProgress : Fiber, ignoreUnmountingErrors : boolean) : ?Fiber {
318
// The current, flushed, state of this fiber is the alternate.
319
// Ideally nothing should rely on this, but relying on it here
320
// means that we don't need an additional field on the work in
@@ -302,7 +335,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
335
ReactFiberInstrumentation.debugTool.onWillCompleteWork(workInProgress);
336
}
337
// If this doesn't spawn new work, complete the current work.
305
- next = completeUnitOfWork(workInProgress);
338
+ next = completeUnitOfWork(workInProgress, ignoreUnmountingErrors);
339
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
340
ReactFiberInstrumentation.debugTool.onDidCompleteWork(workInProgress);
341
}
@@ -313,13 +346,13 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
346
return next;
347
}
348
316
- function performDeferredWork(deadline) {
349
+ function performDeferredWorkUnsafe(deadline) {
350
if (!nextUnitOfWork) {
351
nextUnitOfWork = findNextUnitOfWork();
352
}
353
while (nextUnitOfWork) {
354
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
322
- nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
355
+ nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false);
356
if (!nextUnitOfWork) {
357
// Find more work. We might have time to complete some more.
358
nextUnitOfWork = findNextUnitOfWork();
@@ -331,6 +364,23 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
364
}
365
}
366
367
+ function performDeferredWork(deadline) {
368
+ try {
369
+ performDeferredWorkUnsafe(deadline);
370
+ } catch (error) {
371
+ const failedUnitOfWork = nextUnitOfWork;
372
+ // Reset because it points to the error boundary:
373
+ nextUnitOfWork = null;
374
+ if (!failedUnitOfWork) {
375
+ // We shouldn't end up here because nextUnitOfWork
376
+ // should always be set while work is being performed.
377
+ throw error;
378
+ }
379
+ const trappedError = trapError(failedUnitOfWork, error);
380
+ handleError(trappedError);
381
+ }
382
+ }
383
+
384
function scheduleDeferredWork(root : FiberRoot, priority : PriorityLevel) {
385
// We must reset the current unit of work pointer so that we restart the
386
// search from the root during the next tick, in case there is now higher
@@ -362,12 +412,12 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
412
}
413
}
414
365
- function performAnimationWork() {
415
+ function performAnimationWorkUnsafe() {
416
// Always start from the root
417
nextUnitOfWork = findNextUnitOfWork();
418
while (nextUnitOfWork &&
419
nextPriorityLevel !== NoWork) {
370
- nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
420
+ nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false);
421
if (!nextUnitOfWork) {
422
// Keep searching for animation work until there's no more left
423
nextUnitOfWork = findNextUnitOfWork();
@@ -380,6 +430,23 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
430
}
431
}
432
433
+ function performAnimationWork() {
434
+ try {
435
+ performAnimationWorkUnsafe();
436
+ } catch (error) {
437
+ const failedUnitOfWork = nextUnitOfWork;
438
+ // Reset because it points to the error boundary:
439
+ nextUnitOfWork = null;
440
+ if (!failedUnitOfWork) {
441
+ // We shouldn't end up here because nextUnitOfWork
442
+ // should always be set while work is being performed.
443
+ throw error;
444
+ }
445
+ const trappedError = trapError(failedUnitOfWork, error);
446
+ handleError(trappedError);
447
+ }
448
+ }
449
+
450
function scheduleAnimationWork(root: FiberRoot, priorityLevel : PriorityLevel) {
451
// Set the priority on the root, without deprioritizing
452
if (root.current.pendingWorkPriority === NoWork ||
@@ -404,6 +471,59 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
471
}
472
}
473
474
+ function handleError(trappedError) {
475
+ const boundary = trappedError.boundary;
476
+ const error = trappedError.error;
477
+ if (!boundary) {
478
+ throw error;
479
+ }
480
+
481
+ try {
482
+ // Give error boundary a chance to update its state
483
+ acknowledgeErrorInBoundary(boundary, error);
484
+
485
+ // We will process an update caused by an error boundary with synchronous priority.
486
+ // This leaves us free to not keep track of whether a boundary has errored.
487
+ // If it errors again, we will just catch the error and synchronously propagate it higher.
488
+
489
+ // First, traverse upwards and set pending synchronous priority on the whole tree.
490
+ let fiber = boundary;
491
+ while (fiber) {
492
+ fiber.pendingWorkPriority = SynchronousPriority;
493
+ if (fiber.alternate) {
494
+ fiber.alternate.pendingWorkPriority = SynchronousPriority;
495
+ }
496
+ if (!fiber.return) {
497
+ if (fiber.tag === HostContainer) {
498
+ // We found the root.
499
+ // Now go to the second phase and update it synchronously.
500
+ break;
501
+ } else {
502
+ throw new Error('Invalid root');
503
+ }
504
+ }
505
+ fiber = fiber.return;
506
+ }
507
+
508
+ if (!fiber) {
509
+ throw new Error('Could not find an error boundary root.');
510
+ }
511
+
512
+ // Find the work in progress tree.
513
+ const root : FiberRoot = (fiber.stateNode : any);
514
+ fiber = root.current.alternate;
515
+
516
+ // Perform all the work synchronously.
517
+ while (fiber) {
518
+ fiber = performUnitOfWork(fiber, true);
519
+ }
520
+ } catch (nextError) {
521
+ // Propagate error to the next boundary or rethrow.
522
+ const nextTrappedError = trapError(boundary, nextError);
523
+ handleError(nextTrappedError);
524
+ }
525
+ }
526
+
527
function scheduleWork(root : FiberRoot) {
528
if (defaultPriority === SynchronousPriority) {
529
throw new Error('Not implemented yet');
src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js
+358
-160
@@ -11,6 +11,8 @@
11
12
'use strict';
13
14
+var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
15
+
16
var React;
17
var ReactDOM;
18
@@ -33,6 +35,9 @@ describe('ReactErrorBoundaries', () => {
35
var Normal;
36
37
beforeEach(() => {
38
+ // TODO: Fiber isn't error resilient and one test can bring down them all.
39
+ jest.resetModuleRegistry();
40
+
41
ReactDOM = require('ReactDOM');
42
React = require('React');
43
@@ -46,7 +51,7 @@ describe('ReactErrorBoundaries', () => {
51
}
52
render() {
53
log.push('BrokenConstructor render');
49
- return <div />;
54
+ return <div>{this.props.children}</div>;
55
}
56
componentWillMount() {
57
log.push('BrokenConstructor componentWillMount');
@@ -75,7 +80,7 @@ describe('ReactErrorBoundaries', () => {
80
}
81
render() {
82
log.push('BrokenComponentWillMount render');
78
- return <div />;
83
+ return <div>{this.props.children}</div>;
84
}
85
componentWillMount() {
86
log.push('BrokenComponentWillMount componentWillMount [!]');
@@ -105,7 +110,7 @@ describe('ReactErrorBoundaries', () => {
110
}
111
render() {
112
log.push('BrokenComponentDidMount render');
108
- return <div />;
113
+ return <div>{this.props.children}</div>;
114
}
115
componentWillMount() {
116
log.push('BrokenComponentDidMount componentWillMount');
@@ -135,7 +140,7 @@ describe('ReactErrorBoundaries', () => {
140
}
141
render() {
142
log.push('BrokenComponentWillReceiveProps render');
138
- return <div />;
143
+ return <div>{this.props.children}</div>;
144
}
145
componentWillMount() {
146
log.push('BrokenComponentWillReceiveProps componentWillMount');
@@ -165,7 +170,7 @@ describe('ReactErrorBoundaries', () => {
170
}
171
render() {
172
log.push('BrokenComponentWillUpdate render');
168
- return <div />;
173
+ return <div>{this.props.children}</div>;
174
}
175
componentWillMount() {
176
log.push('BrokenComponentWillUpdate componentWillMount');
@@ -195,7 +200,7 @@ describe('ReactErrorBoundaries', () => {
200
}
201
render() {
202
log.push('BrokenComponentDidUpdate render');
198
- return <div />;
203
+ return <div>{this.props.children}</div>;
204
}
205
componentWillMount() {
206
log.push('BrokenComponentDidUpdate componentWillMount');
@@ -225,7 +230,7 @@ describe('ReactErrorBoundaries', () => {
230
}
231
render() {
232
log.push('BrokenComponentWillUnmount render');
228
- return <div />;
233
+ return <div>{this.props.children}</div>;
234
}
235
componentWillMount() {
236
log.push('BrokenComponentWillUnmount componentWillMount');
@@ -459,52 +464,52 @@ describe('ReactErrorBoundaries', () => {
464
};
465
});
466
462
- // Known limitation: error boundary only "sees" errors caused by updates
463
- // flowing through it. This might be easier to fix in Fiber.
464
- it('currently does not catch errors originating downstream', () => {
465
- var fail = false;
466
- class Stateful extends React.Component {
467
- state = {shouldThrow: false};
468
-
469
- render() {
470
- if (fail) {
471
- log.push('Stateful render [!]');
472
- throw new Error('Hello');
467
+ if (ReactDOMFeatureFlags.useFiber) {
468
+ // This test implements a new feature in Fiber.
469
+ it('catches errors originating downstream', () => {
470
+ var fail = false;
471
+ class Stateful extends React.Component {
472
+ state = {shouldThrow: false};
473
+
474
+ render() {
475
+ if (fail) {
476
+ log.push('Stateful render [!]');
477
+ throw new Error('Hello');
478
+ }
479
+ return <div>{this.props.children}</div>;
480
}
474
- return <div />;
481
}
476
- }
482
478
- var statefulInst;
479
- var container = document.createElement('div');
480
- ReactDOM.render(
481
- <ErrorBoundary>
482
- <Stateful ref={inst => statefulInst = inst} />
483
- </ErrorBoundary>,
484
- container
485
- );
486
-
487
- log.length = 0;
488
- expect(() => {
489
- fail = true;
490
- statefulInst.forceUpdate();
491
- }).toThrow();
492
-
493
- expect(log).toEqual([
494
- 'Stateful render [!]',
495
- // FIXME: uncomment when downstream errors get caught.
496
- // Catch and render an error message
497
- // 'ErrorBoundary unstable_handleError',
498
- // 'ErrorBoundary render error',
499
- // 'ErrorBoundary componentDidUpdate',
500
- ]);
483
+ var statefulInst;
484
+ var container = document.createElement('div');
485
+ ReactDOM.render(
486
+ <ErrorBoundary>
487
+ <Stateful ref={inst => statefulInst = inst} />
488
+ </ErrorBoundary>,
489
+ container
490
+ );
491
502
- log.length = 0;
503
- ReactDOM.unmountComponentAtNode(container);
504
- expect(log).toEqual([
505
- 'ErrorBoundary componentWillUnmount',
506
- ]);
507
- });
492
+ log.length = 0;
493
+ expect(() => {
494
+ fail = true;
495
+ statefulInst.forceUpdate();
496
+ }).not.toThrow();
497
+
498
+ expect(log).toEqual([
499
+ 'Stateful render [!]',
500
+ 'ErrorBoundary unstable_handleError',
501
+ 'ErrorBoundary componentWillUpdate',
502
+ 'ErrorBoundary render error',
503
+ 'ErrorBoundary componentDidUpdate',
504
+ ]);
505
+
506
+ log.length = 0;
507
+ ReactDOM.unmountComponentAtNode(container);
508
+ expect(log).toEqual([
509
+ 'ErrorBoundary componentWillUnmount',
510
+ ]);
511
+ });
512
+ }
513
514
it('renders an error state if child throws in render', () => {
515
var container = document.createElement('div');
@@ -526,6 +531,12 @@ describe('ReactErrorBoundaries', () => {
531
'BrokenRender render [!]',
532
// Catch and render an error message
533
'ErrorBoundary unstable_handleError',
534
+ ...(ReactDOMFeatureFlags.useFiber ? [
535
+ // The initial render was aborted, so
536
+ // Fiber retries from the root.
537
+ 'ErrorBoundary constructor',
538
+ 'ErrorBoundary componentWillMount',
539
+ ] : []),
540
'ErrorBoundary render error',
541
'ErrorBoundary componentDidMount',
542
]);
@@ -553,6 +564,12 @@ describe('ReactErrorBoundaries', () => {
564
'BrokenConstructor constructor [!]',
565
// Catch and render an error message
566
'ErrorBoundary unstable_handleError',
567
+ ...(ReactDOMFeatureFlags.useFiber ? [
568
+ // The initial render was aborted, so
569
+ // Fiber retries from the root.
570
+ 'ErrorBoundary constructor',
571
+ 'ErrorBoundary componentWillMount',
572
+ ] : []),
573
'ErrorBoundary render error',
574
'ErrorBoundary componentDidMount',
575
]);
@@ -581,6 +598,12 @@ describe('ReactErrorBoundaries', () => {
598
'BrokenComponentWillMount componentWillMount [!]',
599
// Catch and render an error message
600
'ErrorBoundary unstable_handleError',
601
+ ...(ReactDOMFeatureFlags.useFiber ? [
602
+ // The initial render was aborted, so
603
+ // Fiber retries from the root.
604
+ 'ErrorBoundary constructor',
605
+ 'ErrorBoundary componentWillMount',
606
+ ] : []),
607
'ErrorBoundary render error',
608
'ErrorBoundary componentDidMount',
609
]);
@@ -615,6 +638,12 @@ describe('ReactErrorBoundaries', () => {
638
'BrokenRender render [!]',
639
// Handle the error:
640
'ErrorBoundary unstable_handleError',
641
+ ...(ReactDOMFeatureFlags.useFiber ? [
642
+ // The initial render was aborted, so
643
+ // Fiber retries from the root.
644
+ 'ErrorBoundary constructor',
645
+ 'ErrorBoundary componentWillMount',
646
+ ] : []),
647
'ErrorBoundary render error',
648
// Mount the error message:
649
'ErrorMessage constructor',
@@ -632,40 +661,64 @@ describe('ReactErrorBoundaries', () => {
661
]);
662
});
663
635
- // Known limitation because componentDidMount() does not occur on the stack.
636
- // We could either hardcode searching for parent boundary, or wait for Fiber.
637
- it('currently does not catch errors in componentDidMount', () => {
638
- var container = document.createElement('div');
639
- expect(() => {
664
+ if (ReactDOMFeatureFlags.useFiber) {
665
+ // This test implements a new feature in Fiber.
666
+ it('catches errors in componentDidMount', () => {
667
+ var container = document.createElement('div');
668
ReactDOM.render(
669
<ErrorBoundary>
670
+ <BrokenComponentWillUnmount>
671
+ <Normal />
672
+ </BrokenComponentWillUnmount>
673
<BrokenComponentDidMount />
674
+ <Normal logName="LastChild" />
675
</ErrorBoundary>,
676
container
677
);
646
- }).toThrow();
647
- expect(log).toEqual([
648
- 'ErrorBoundary constructor',
649
- 'ErrorBoundary componentWillMount',
650
- 'ErrorBoundary render success',
651
- 'BrokenComponentDidMount constructor',
652
- 'BrokenComponentDidMount componentWillMount',
653
- 'BrokenComponentDidMount render',
654
- 'BrokenComponentDidMount componentDidMount [!]',
655
- // FIXME: uncomment when componentDidMount() gets caught.
656
- // Catch and render an error message
657
- // 'ErrorBoundary unstable_handleError',
658
- // 'ErrorBoundary render error',
659
- // 'ErrorBoundary componentDidMount',
660
- ]);
661
-
662
- log.length = 0;
663
- ReactDOM.unmountComponentAtNode(container);
664
- expect(log).toEqual([
665
- 'ErrorBoundary componentWillUnmount',
666
- 'BrokenComponentDidMount componentWillUnmount',
667
- ]);
668
- });
678
+ expect(log).toEqual([
679
+ 'ErrorBoundary constructor',
680
+ 'ErrorBoundary componentWillMount',
681
+ 'ErrorBoundary render success',
682
+ 'BrokenComponentWillUnmount constructor',
683
+ 'BrokenComponentWillUnmount componentWillMount',
684
+ 'BrokenComponentWillUnmount render',
685
+ 'Normal constructor',
686
+ 'Normal componentWillMount',
687
+ 'Normal render',
688
+ 'BrokenComponentDidMount constructor',
689
+ 'BrokenComponentDidMount componentWillMount',
690
+ 'BrokenComponentDidMount render',
691
+ 'LastChild constructor',
692
+ 'LastChild componentWillMount',
693
+ 'LastChild render',
694
+ // Start flushing didMount queue
695
+ 'Normal componentDidMount',
696
+ 'BrokenComponentWillUnmount componentDidMount',
697
+ 'BrokenComponentDidMount componentDidMount [!]',
698
+ // Continue despite the error
699
+ 'LastChild componentDidMount',
700
+ 'ErrorBoundary componentDidMount',
701
+ // Now we are ready to handle the error
702
+ 'ErrorBoundary unstable_handleError',
703
+ 'ErrorBoundary componentWillUpdate',
704
+ 'ErrorBoundary render error',
705
+ // Safely unmount every child
706
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
707
+ // Continue unmounting safely despite any errors
708
+ 'Normal componentWillUnmount',
709
+ 'BrokenComponentDidMount componentWillUnmount',
710
+ 'LastChild componentWillUnmount',
711
+ // The update has finished
712
+ 'ErrorBoundary componentDidUpdate',
713
+ ]);
714
+
715
+ log.length = 0;
716
+ ReactDOM.unmountComponentAtNode(container);
717
+ expect(log).toEqual([
718
+ 'ErrorBoundary componentWillUnmount',
719
+ ]);
720
+ });
721
+ }
722
723
it('propagates errors on retry on mounting', () => {
724
var container = document.createElement('div');
@@ -688,15 +741,30 @@ describe('ReactErrorBoundaries', () => {
741
'BrokenRender constructor',
742
'BrokenRender componentWillMount',
743
'BrokenRender render [!]',
691
- // The first error boundary catches the error
692
- // However, it doesn't adjust its state so next render also fails
744
+ // The first error boundary catches the error.
745
+ // However, it doesn't adjust its state so next render will also fail.
746
'NoopErrorBoundary unstable_handleError',
747
+ ...(ReactDOMFeatureFlags.useFiber ? [
748
+ // The initial render was aborted, so
749
+ // Fiber retries from the root.
750
+ 'ErrorBoundary constructor',
751
+ 'ErrorBoundary componentWillMount',
752
+ 'ErrorBoundary render success',
753
+ 'NoopErrorBoundary constructor',
754
+ 'NoopErrorBoundary componentWillMount',
755
+ ] : []),
756
'NoopErrorBoundary render',
757
'BrokenRender constructor',
758
'BrokenRender componentWillMount',
759
'BrokenRender render [!]',
760
// This time, the error propagates to the higher boundary
761
'ErrorBoundary unstable_handleError',
762
+ ...(ReactDOMFeatureFlags.useFiber ? [
763
+ // The initial render was aborted, so
764
+ // Fiber retries from the root.
765
+ 'ErrorBoundary constructor',
766
+ 'ErrorBoundary componentWillMount',
767
+ ] : []),
768
// Render the error
769
'ErrorBoundary render error',
770
'ErrorBoundary componentDidMount',
@@ -726,6 +794,12 @@ describe('ReactErrorBoundaries', () => {
794
'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
795
// The error propagates to the higher boundary
796
'ErrorBoundary unstable_handleError',
797
+ ...(ReactDOMFeatureFlags.useFiber ? [
798
+ // The initial render was aborted, so
799
+ // Fiber retries from the root.
800
+ 'ErrorBoundary constructor',
801
+ 'ErrorBoundary componentWillMount',
802
+ ] : []),
803
// Render the error
804
'ErrorBoundary render error',
805
'ErrorBoundary componentDidMount',
@@ -762,9 +836,24 @@ describe('ReactErrorBoundaries', () => {
836
// The first error boundary catches the error
837
// It adjusts state but throws displaying the message
838
'BrokenRenderErrorBoundary unstable_handleError',
839
+ ...(ReactDOMFeatureFlags.useFiber ? [
840
+ // The initial render was aborted, so
841
+ // Fiber retries from the root.
842
+ 'ErrorBoundary constructor',
843
+ 'ErrorBoundary componentWillMount',
844
+ 'ErrorBoundary render success',
845
+ 'BrokenRenderErrorBoundary constructor',
846
+ 'BrokenRenderErrorBoundary componentWillMount',
847
+ ] : []),
848
'BrokenRenderErrorBoundary render error [!]',
849
// The error propagates to the higher boundary
850
'ErrorBoundary unstable_handleError',
851
+ ...(ReactDOMFeatureFlags.useFiber ? [
852
+ // The initial render was aborted, so
853
+ // Fiber retries from the root.
854
+ 'ErrorBoundary constructor',
855
+ 'ErrorBoundary componentWillMount',
856
+ ] : []),
857
// Render the error
858
'ErrorBoundary render error',
859
'ErrorBoundary componentDidMount',
@@ -822,6 +911,12 @@ describe('ReactErrorBoundaries', () => {
911
'BrokenRender render [!]',
912
// Error boundary catches the error
913
'ErrorBoundary unstable_handleError',
914
+ ...(ReactDOMFeatureFlags.useFiber ? [
915
+ // The initial render was aborted, so
916
+ // Fiber retries from the root.
917
+ 'ErrorBoundary constructor',
918
+ 'ErrorBoundary componentWillMount',
919
+ ] : []),
920
// Render the error message
921
'ErrorBoundary render error',
922
'ErrorBoundary componentDidMount',
@@ -860,8 +955,16 @@ describe('ReactErrorBoundaries', () => {
955
'BrokenRender render [!]',
956
// Handle error:
957
'ErrorBoundary unstable_handleError',
863
- // Child ref wasn't (and won't be) set but there's no harm in clearing:
864
- 'Child ref is set to null',
958
+ ...(ReactDOMFeatureFlags.useFiber ? [
959
+ // The initial render was aborted, so
960
+ // Fiber retries from the root.
961
+ 'ErrorBoundary constructor',
962
+ 'ErrorBoundary componentWillMount',
963
+ ] : [
964
+ // Stack reconciler resets ref on update, as it doesn't know ref was never set.
965
+ // This is unnecessary, and Fiber doesn't do it:
966
+ 'Child ref is set to null',
967
+ ]),
968
'ErrorBoundary render error',
969
// Ref to error message should get set:
970
'Error message ref is set to [object HTMLDivElement]',
@@ -932,10 +1035,20 @@ describe('ReactErrorBoundaries', () => {
1035
// BrokenConstructor will abort rendering:
1036
'BrokenConstructor constructor [!]',
1037
'ErrorBoundary unstable_handleError',
935
- // Unmount the previously mounted components:
936
- 'Normal componentWillUnmount',
1038
+ ...(ReactDOMFeatureFlags.useFiber ? [
1039
+ // The initial render was aborted, so
1040
+ // Fiber retries from the root.
1041
+ 'ErrorBoundary componentWillReceiveProps',
1042
+ 'ErrorBoundary componentWillUpdate',
1043
+ // Fiber renders first, then unmounts in a batch:
1044
+ 'ErrorBoundary render error',
1045
+ 'Normal componentWillUnmount',
1046
+ ] : [
1047
+ // Stack unmounts first, then renders:
1048
+ 'Normal componentWillUnmount',
1049
+ 'ErrorBoundary render error',
1050
+ ]),
1051
// Normal2 does not get lifefycle because it was never mounted
938
- 'ErrorBoundary render error',
1052
'ErrorBoundary componentDidUpdate',
1053
]);
1054
@@ -980,10 +1093,20 @@ describe('ReactErrorBoundaries', () => {
1093
'BrokenComponentWillMount constructor',
1094
'BrokenComponentWillMount componentWillMount [!]',
1095
'ErrorBoundary unstable_handleError',
983
- // Unmount the previously mounted components:
984
- 'Normal componentWillUnmount',
1096
+ ...(ReactDOMFeatureFlags.useFiber ? [
1097
+ // The initial render was aborted, so
1098
+ // Fiber retries from the root.
1099
+ 'ErrorBoundary componentWillReceiveProps',
1100
+ 'ErrorBoundary componentWillUpdate',
1101
+ // Fiber renders first, then unmounts in a batch:
1102
+ 'ErrorBoundary render error',
1103
+ 'Normal componentWillUnmount',
1104
+ ] : [
1105
+ // Stack unmounts first, then renders:
1106
+ 'Normal componentWillUnmount',
1107
+ 'ErrorBoundary render error',
1108
+ ]),
1109
// Normal2 does not get lifefycle because it was never mounted
986
- 'ErrorBoundary render error',
1110
'ErrorBoundary componentDidUpdate',
1111
]);
1112
@@ -1023,11 +1146,21 @@ describe('ReactErrorBoundaries', () => {
1146
// BrokenComponentWillReceiveProps will abort rendering:
1147
'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
1148
'ErrorBoundary unstable_handleError',
1026
- // Unmount the previously mounted components:
1027
- 'Normal componentWillUnmount',
1028
- 'BrokenComponentWillReceiveProps componentWillUnmount',
1029
- // Render error:
1030
- 'ErrorBoundary render error',
1149
+ ...(ReactDOMFeatureFlags.useFiber ? [
1150
+ // The initial render was aborted, so
1151
+ // Fiber retries from the root.
1152
+ 'ErrorBoundary componentWillReceiveProps',
1153
+ 'ErrorBoundary componentWillUpdate',
1154
+ // Fiber renders first, then unmounts in a batch:
1155
+ 'ErrorBoundary render error',
1156
+ 'Normal componentWillUnmount',
1157
+ 'BrokenComponentWillReceiveProps componentWillUnmount',
1158
+ ] : [
1159
+ // Stack unmounts first, then renders:
1160
+ 'Normal componentWillUnmount',
1161
+ 'BrokenComponentWillReceiveProps componentWillUnmount',
1162
+ 'ErrorBoundary render error',
1163
+ ]),
1164
'ErrorBoundary componentDidUpdate',
1165
]);
1166
@@ -1068,11 +1201,21 @@ describe('ReactErrorBoundaries', () => {
1201
'BrokenComponentWillUpdate componentWillReceiveProps',
1202
'BrokenComponentWillUpdate componentWillUpdate [!]',
1203
'ErrorBoundary unstable_handleError',
1071
- // Unmount the previously mounted components:
1072
- 'Normal componentWillUnmount',
1073
- 'BrokenComponentWillUpdate componentWillUnmount',
1074
- // Render error:
1075
- 'ErrorBoundary render error',
1204
+ ...(ReactDOMFeatureFlags.useFiber ? [
1205
+ // The initial render was aborted, so
1206
+ // Fiber retries from the root.
1207
+ 'ErrorBoundary componentWillReceiveProps',
1208
+ 'ErrorBoundary componentWillUpdate',
1209
+ // Fiber renders first, then unmounts in a batch:
1210
+ 'ErrorBoundary render error',
1211
+ 'Normal componentWillUnmount',
1212
+ 'BrokenComponentWillUpdate componentWillUnmount',
1213
+ ] : [
1214
+ // Stack unmounts first, then renders:
1215
+ 'Normal componentWillUnmount',
1216
+ 'BrokenComponentWillUpdate componentWillUnmount',
1217
+ 'ErrorBoundary render error',
1218
+ ]),
1219
'ErrorBoundary componentDidUpdate',
1220
]);
1221
@@ -1118,10 +1261,20 @@ describe('ReactErrorBoundaries', () => {
1261
'BrokenRender componentWillMount',
1262
'BrokenRender render [!]',
1263
'ErrorBoundary unstable_handleError',
1121
- // Unmount the previously mounted components:
1122
- 'Normal componentWillUnmount',
1264
+ ...(ReactDOMFeatureFlags.useFiber ? [
1265
+ // The initial render was aborted, so
1266
+ // Fiber retries from the root.
1267
+ 'ErrorBoundary componentWillReceiveProps',
1268
+ 'ErrorBoundary componentWillUpdate',
1269
+ // Fiber renders first, then unmounts in a batch:
1270
+ 'ErrorBoundary render error',
1271
+ 'Normal componentWillUnmount',
1272
+ ] : [
1273
+ // Stack unmounts first, then renders:
1274
+ 'Normal componentWillUnmount',
1275
+ 'ErrorBoundary render error',
1276
+ ]),
1277
// Normal2 does not get lifefycle because it was never mounted
1124
- 'ErrorBoundary render error',
1278
'ErrorBoundary componentDidUpdate',
1279
]);
1280
@@ -1177,9 +1330,19 @@ describe('ReactErrorBoundaries', () => {
1330
'BrokenRender componentWillMount',
1331
'BrokenRender render [!]',
1332
'ErrorBoundary unstable_handleError',
1180
- // Unmount the previously mounted components:
1181
- 'Child1 ref is set to null',
1182
- 'ErrorBoundary render error',
1333
+ ...(ReactDOMFeatureFlags.useFiber ? [
1334
+ // The initial render was aborted, so
1335
+ // Fiber retries from the root.
1336
+ 'ErrorBoundary componentWillReceiveProps',
1337
+ 'ErrorBoundary componentWillUpdate',
1338
+ // Fiber renders first, resets refs later
1339
+ 'ErrorBoundary render error',
1340
+ 'Child1 ref is set to null',
1341
+ ] : [
1342
+ // Stack resets ref first, renders later
1343
+ 'Child1 ref is set to null',
1344
+ 'ErrorBoundary render error',
1345
+ ]),
1346
'Error message ref is set to [object HTMLDivElement]',
1347
// Child2 ref is never set because its mounting aborted
1348
'ErrorBoundary componentDidUpdate',
@@ -1193,48 +1356,49 @@ describe('ReactErrorBoundaries', () => {
1356
]);
1357
});
1358
1196
- // Known limitation because componentDidUpdate() does not occur on the stack.
1197
- // We could either hardcode searching for parent boundary, or wait for Fiber.
1198
- it('currently does not catch errors in componentDidUpdate', () => {
1199
- var container = document.createElement('div');
1200
- ReactDOM.render(
1201
- <ErrorBoundary>
1202
- <BrokenComponentDidUpdate />
1203
- </ErrorBoundary>,
1204
- container
1205
- );
1206
-
1207
- log.length = 0;
1208
- expect(() => {
1359
+ if (ReactDOMFeatureFlags.useFiber) {
1360
+ // This test implements a new feature in Fiber.
1361
+ it('catches errors in componentDidUpdate', () => {
1362
+ var container = document.createElement('div');
1363
ReactDOM.render(
1364
<ErrorBoundary>
1365
<BrokenComponentDidUpdate />
1366
</ErrorBoundary>,
1367
container
1368
);
1215
- }).toThrow();
1216
- expect(log).toEqual([
1217
- 'ErrorBoundary componentWillReceiveProps',
1218
- 'ErrorBoundary componentWillUpdate',
1219
- 'ErrorBoundary render success',
1220
- 'BrokenComponentDidUpdate componentWillReceiveProps',
1221
- 'BrokenComponentDidUpdate componentWillUpdate',
1222
- 'BrokenComponentDidUpdate render',
1223
- 'BrokenComponentDidUpdate componentDidUpdate [!]',
1224
- // FIXME: uncomment when componentDidUpdate() gets caught.
1225
- // Catch and render an error message
1226
- // 'ErrorBoundary unstable_handleError',
1227
- // 'ErrorBoundary render error',
1228
- // 'ErrorBoundary componentDidUpdate',
1229
- ]);
1369
1231
- log.length = 0;
1232
- ReactDOM.unmountComponentAtNode(container);
1233
- expect(log).toEqual([
1234
- 'ErrorBoundary componentWillUnmount',
1235
- 'BrokenComponentDidUpdate componentWillUnmount',
1236
- ]);
1237
- });
1370
+ log.length = 0;
1371
+ ReactDOM.render(
1372
+ <ErrorBoundary>
1373
+ <BrokenComponentDidUpdate />
1374
+ </ErrorBoundary>,
1375
+ container
1376
+ );
1377
+ expect(log).toEqual([
1378
+ 'ErrorBoundary componentWillReceiveProps',
1379
+ 'ErrorBoundary componentWillUpdate',
1380
+ 'ErrorBoundary render success',
1381
+ 'BrokenComponentDidUpdate componentWillReceiveProps',
1382
+ 'BrokenComponentDidUpdate componentWillUpdate',
1383
+ 'BrokenComponentDidUpdate render',
1384
+ // All lifecycles run
1385
+ 'BrokenComponentDidUpdate componentDidUpdate [!]',
1386
+ 'ErrorBoundary componentDidUpdate',
1387
+ // Then, error is handled
1388
+ 'ErrorBoundary unstable_handleError',
1389
+ 'ErrorBoundary componentWillUpdate',
1390
+ 'ErrorBoundary render error',
1391
+ 'BrokenComponentDidUpdate componentWillUnmount',
1392
+ 'ErrorBoundary componentDidUpdate',
1393
+ ]);
1394
+
1395
+ log.length = 0;
1396
+ ReactDOM.unmountComponentAtNode(container);
1397
+ expect(log).toEqual([
1398
+ 'ErrorBoundary componentWillUnmount',
1399
+ ]);
1400
+ });
1401
+ }
1402
1403
it('recovers from componentWillUnmount errors on update', () => {
1404
var container = document.createElement('div');
@@ -1242,7 +1406,7 @@ describe('ReactErrorBoundaries', () => {
1406
<ErrorBoundary>
1407
<BrokenComponentWillUnmount />
1408
<BrokenComponentWillUnmount />
1245
- <BrokenComponentWillUnmount />
1409
+ <Normal />
1410
</ErrorBoundary>,
1411
container
1412
);
@@ -1251,7 +1415,6 @@ describe('ReactErrorBoundaries', () => {
1415
ReactDOM.render(
1416
<ErrorBoundary>
1417
<BrokenComponentWillUnmount />
1254
- <BrokenComponentWillUnmount />
1418
</ErrorBoundary>,
1419
container
1420
);
@@ -1260,23 +1423,39 @@ describe('ReactErrorBoundaries', () => {
1423
'ErrorBoundary componentWillReceiveProps',
1424
'ErrorBoundary componentWillUpdate',
1425
'ErrorBoundary render success',
1263
- // Update existing children:
1264
- 'BrokenComponentWillUnmount componentWillReceiveProps',
1265
- 'BrokenComponentWillUnmount componentWillUpdate',
1266
- 'BrokenComponentWillUnmount render',
1426
+ // Update existing child:
1427
'BrokenComponentWillUnmount componentWillReceiveProps',
1428
'BrokenComponentWillUnmount componentWillUpdate',
1429
'BrokenComponentWillUnmount render',
1430
// Unmounting throws:
1431
'BrokenComponentWillUnmount componentWillUnmount [!]',
1272
- 'ErrorBoundary unstable_handleError',
1273
- // Attempt to unmount previous children:
1274
- 'BrokenComponentWillUnmount componentWillUnmount [!]',
1275
- 'BrokenComponentWillUnmount componentWillUnmount [!]',
1276
- // Render error:
1277
- 'ErrorBoundary render error',
1278
- 'ErrorBoundary componentDidUpdate',
1279
- // Children don't get componentDidUpdate() since update was aborted
1432
+ ...(ReactDOMFeatureFlags.useFiber ? [
1433
+ // Fiber proceeds with lifecycles despite errors
1434
+ 'Normal componentWillUnmount',
1435
+ // The components have updated in this phase
1436
+ 'BrokenComponentWillUnmount componentDidUpdate',
1437
+ 'ErrorBoundary componentDidUpdate',
1438
+ // Now that commit phase is done, Fiber handles errors
1439
+ 'ErrorBoundary unstable_handleError',
1440
+ // The initial render was aborted, so
1441
+ // Fiber retries from the root.
1442
+ 'ErrorBoundary componentWillUpdate',
1443
+ // Render an error now (stack will do it later)
1444
+ 'ErrorBoundary render error',
1445
+ // Attempt to unmount previous child:
1446
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1447
+ // Done
1448
+ 'ErrorBoundary componentDidUpdate',
1449
+ ] : [
1450
+ // Stack will handle error immediately
1451
+ 'ErrorBoundary unstable_handleError',
1452
+ // Attempt to unmount previous children:
1453
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1454
+ 'Normal componentWillUnmount',
1455
+ // Render an error now (Fiber will do it earlier)
1456
+ 'ErrorBoundary render error',
1457
+ 'ErrorBoundary componentDidUpdate',
1458
+ ]),
1459
]);
1460
1461
log.length = 0;
@@ -1321,13 +1500,32 @@ describe('ReactErrorBoundaries', () => {
1500
'BrokenComponentWillUnmount render',
1501
// Unmounting throws:
1502
'BrokenComponentWillUnmount componentWillUnmount [!]',
1324
- 'ErrorBoundary unstable_handleError',
1325
- // Attempt to unmount previous children:
1326
- 'Normal componentWillUnmount',
1327
- 'BrokenComponentWillUnmount componentWillUnmount [!]',
1328
- // Render error:
1329
- 'ErrorBoundary render error',
1330
- 'ErrorBoundary componentDidUpdate',
1503
+ ...(ReactDOMFeatureFlags.useFiber ? [
1504
+ // Fiber proceeds with lifecycles despite errors
1505
+ 'BrokenComponentWillUnmount componentDidUpdate',
1506
+ 'Normal componentDidUpdate',
1507
+ 'ErrorBoundary componentDidUpdate',
1508
+ // Now that commit phase is done, Fiber handles errors
1509
+ 'ErrorBoundary unstable_handleError',
1510
+ // The initial render was aborted, so
1511
+ // Fiber retries from the root.
1512
+ 'ErrorBoundary componentWillUpdate',
1513
+ // Render an error now (stack will do it later)
1514
+ 'ErrorBoundary render error',
1515
+ // Attempt to unmount previous child:
1516
+ 'Normal componentWillUnmount',
1517
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1518
+ // Done
1519
+ 'ErrorBoundary componentDidUpdate',
1520
+ ] : [
1521
+ 'ErrorBoundary unstable_handleError',
1522
+ // Attempt to unmount previous children:
1523
+ 'Normal componentWillUnmount',
1524
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1525
+ // Stack calls lifecycles first, then renders.
1526
+ 'ErrorBoundary render error',
1527
+ 'ErrorBoundary componentDidUpdate',
1528
+ ]),
1529
]);
1530
1531
log.length = 0;
@@ -1474,7 +1672,7 @@ describe('ReactErrorBoundaries', () => {
1672
if (fail) {
1673
throw new Error('Hello');
1674
}
1477
- return <div />;
1675
+ return <div>{this.props.children}</div>;
1676
}
1677
}
1678