@samitouri / QOS-React / commits / 3c6abbfff7

[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