Skip to content

Commit 6293042

Browse files
committed
Throw away a failed tree before attempting to recover
When an error is thrown and caught by a boundary, we don't want to reuse the failed work when attempting to recover, because parts of the tree might be in an inconsistent state. Instead, we force rerender the entire tree from scratch. A similar thing happens with failed roots. When a tree fails with an uncaught error, the entire tree is force unmounted. But we don't want to actually remove the tree from the host environment, because otherwise the user would see an empty screen -- unlike the error boundary case, there's nothing to replace the failed tree with. So we do a "soft" delete, where componentWillUnmount is called recursively, but the host nodes remain. The next time something is rendered into the container, the old nodes are deleted.
1 parent da021ca commit 6293042

11 files changed

+374
-50
lines changed

scripts/fiber/tests-failing.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ src/renderers/art/__tests__/ReactART-test.js
7272
* resolves refs before componentDidMount
7373
* resolves refs before componentDidUpdate
7474

75+
src/renderers/dom/__tests__/ReactDOMProduction-test.js
76+
* should throw with an error code in production
77+
7578
src/renderers/dom/shared/__tests__/CSSPropertyOperations-test.js
7679
* should set style attribute when styles exist
7780
* should warn when using hyphenated style names

scripts/fiber/tests-passing.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,6 @@ src/renderers/dom/__tests__/ReactDOMProduction-test.js
453453
* should use prod React
454454
* should handle a simple flow
455455
* should call lifecycle methods
456-
* should throw with an error code in production
457456

458457
src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
459458
* should render strings as children
@@ -815,6 +814,8 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
815814
* can schedule updates after uncaught error during umounting
816815
* continues work on other roots despite caught errors
817816
* continues work on other roots despite uncaught errors
817+
* force unmounts failed subtree before rerendering
818+
* force unmounts failed root
818819

819820
src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js
820821
* handles isMounted even when the initial render is deferred

src/renderers/dom/__tests__/ReactDOMProduction-test.js

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe('ReactDOMProduction', () => {
1515

1616
var React;
1717
var ReactDOM;
18+
var ReactDOMFeatureFlags;
1819

1920
beforeEach(() => {
2021
__DEV__ = false;
@@ -24,6 +25,7 @@ describe('ReactDOMProduction', () => {
2425
jest.resetModuleRegistry();
2526
React = require('React');
2627
ReactDOM = require('ReactDOM');
28+
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
2729
});
2830

2931
afterEach(() => {
@@ -174,21 +176,25 @@ describe('ReactDOMProduction', () => {
174176
]);
175177
});
176178

177-
it('should throw with an error code in production', () => {
178-
expect(function() {
179-
class Component extends React.Component {
180-
render() {
181-
return ['this is wrong'];
179+
if (!ReactDOMFeatureFlags.useFiber) {
180+
// This doesn't throw in Fiber because it supports fragments.
181+
// FIXME: Figure out how to test this in Fiber, if possible.
182+
it('should throw with an error code in production', () => {
183+
expect(function() {
184+
class Component extends React.Component {
185+
render() {
186+
return ['this is wrong'];
187+
}
182188
}
183-
}
184189

185-
var container = document.createElement('div');
186-
ReactDOM.render(<Component />, container);
187-
}).toThrowError(
188-
'Minified React error #109; visit ' +
189-
'http://facebook.github.io/react/docs/error-decoder.html?invariant=109&args[]=Component' +
190-
' for the full message or use the non-minified dev environment' +
191-
' for full errors and additional helpful warnings.'
192-
);
193-
});
190+
var container = document.createElement('div');
191+
ReactDOM.render(<Component />, container);
192+
}).toThrowError(
193+
'Minified React error #109; visit ' +
194+
'http://facebook.github.io/react/docs/error-decoder.html?invariant=109&args[]=Component' +
195+
' for the full message or use the non-minified dev environment' +
196+
' for full errors and additional helpful warnings.'
197+
);
198+
});
199+
}
194200
});

src/renderers/shared/fiber/ReactChildFiber.js

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
1616
import type { Fiber } from 'ReactFiber';
1717
import type { PriorityLevel } from 'ReactPriorityLevel';
18+
import type { FiberRoot } from 'ReactFiberRoot';
1819

1920
var REACT_ELEMENT_TYPE = require('ReactElementSymbol');
2021
var {
@@ -26,6 +27,7 @@ var ReactFiber = require('ReactFiber');
2627
var ReactReifiedYield = require('ReactReifiedYield');
2728
var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect');
2829
var ReactTypeOfWork = require('ReactTypeOfWork');
30+
var ReactFiberRootErrorPhase = require('ReactFiberRootErrorPhase');
2931

3032
var emptyObject = require('emptyObject');
3133
var getIteratorFn = require('getIteratorFn');
@@ -52,6 +54,7 @@ const {
5254
CoroutineComponent,
5355
YieldComponent,
5456
Fragment,
57+
HostContainer,
5558
} = ReactTypeOfWork;
5659

5760
const {
@@ -60,6 +63,11 @@ const {
6063
Deletion,
6164
} = ReactTypeOfSideEffect;
6265

66+
const {
67+
NoError,
68+
HardDeletion,
69+
} = ReactFiberRootErrorPhase;
70+
6371
function transferRef(current: ?Fiber, workInProgress: Fiber, element: ReactElement<any>) {
6472
if (typeof element.ref === 'string') {
6573
if (element._owner) {
@@ -480,12 +488,34 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
480488
return null;
481489
}
482490

491+
function isFailedErrorBoundary(fiber : Fiber): boolean {
492+
// Detect if the return fiber is a failed error boundary. If so, we should
493+
// treat the keys as if they don't match to force a remount. For convenience
494+
// this check will reset the error boundary flag, so make sure to only call
495+
// this once per reconciliation.
496+
switch (fiber.tag) {
497+
case ClassComponent:
498+
if (fiber.stateNode._isFailedErrorBoundary) {
499+
delete fiber.stateNode._isFailedErrorBoundary;
500+
return true;
501+
}
502+
return false;
503+
case HostContainer:
504+
const root : FiberRoot = fiber.stateNode;
505+
if (root.errorPhase === HardDeletion) {
506+
root.errorPhase = NoError;
507+
return true;
508+
}
509+
return false;
510+
}
511+
return false;
512+
}
513+
483514
function reconcileChildrenArray(
484515
returnFiber : Fiber,
485516
currentFirstChild : ?Fiber,
486517
newChildren : Array<*>,
487518
priority : PriorityLevel) : ?Fiber {
488-
489519
// This algorithm can't optimize by searching from boths ends since we
490520
// don't have backpointers on fibers. I'm trying to see how far we can get
491521
// with that model. If it ends up not being worth the tradeoffs, we can
@@ -502,21 +532,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
502532
// In this first iteration, we'll just live with hitting the bad case
503533
// (adding everything to a Map) in for every insert/move.
504534

535+
const returnFiberIsFailedErrorBoundary = isFailedErrorBoundary(returnFiber);
536+
505537
let resultingFirstChild : ?Fiber = null;
506538
let previousNewFiber : ?Fiber = null;
507539

508-
let oldFiber = currentFirstChild;
540+
// If the parent is an error boundary, setting oldFiber to null ensures
541+
// that none of the old fibers are reused, the old children are deleted,
542+
// and new fibers are inserted.
543+
let oldFiber = returnFiberIsFailedErrorBoundary ? null : currentFirstChild;
509544
let lastPlacedIndex = 0;
510545
let newIdx = 0;
511546
let nextOldFiber = null;
512547
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
513-
if (oldFiber) {
514-
if (oldFiber.index > newIdx) {
515-
nextOldFiber = oldFiber;
516-
oldFiber = null;
517-
} else {
518-
nextOldFiber = oldFiber.sibling;
519-
}
548+
if (oldFiber.index > newIdx) {
549+
nextOldFiber = oldFiber;
550+
oldFiber = null;
551+
} else {
552+
nextOldFiber = oldFiber.sibling;
520553
}
521554
const newFiber = updateSlot(
522555
returnFiber,
@@ -670,12 +703,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
670703
element : ReactElement<any>,
671704
priority : PriorityLevel
672705
) : Fiber {
706+
const returnFiberIsFailedErrorBoundary = isFailedErrorBoundary(returnFiber);
673707
const key = element.key;
674708
let child = currentFirstChild;
675709
while (child) {
676710
// TODO: If key === null and child.key === null, then this only applies to
677711
// the first item in the list.
678-
if (child.key === key) {
712+
if (child.key === key && !returnFiberIsFailedErrorBoundary) {
679713
if (child.type === element.type) {
680714
deleteRemainingChildren(returnFiber, child.sibling);
681715
const existing = useFiber(child, priority);
@@ -705,12 +739,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
705739
coroutine : ReactCoroutine,
706740
priority : PriorityLevel
707741
) : Fiber {
742+
const returnFiberIsFailedErrorBoundary = isFailedErrorBoundary(returnFiber);
708743
const key = coroutine.key;
709744
let child = currentFirstChild;
710745
while (child) {
711746
// TODO: If key === null and child.key === null, then this only applies to
712747
// the first item in the list.
713-
if (child.key === key) {
748+
if (child.key === key && !returnFiberIsFailedErrorBoundary) {
714749
if (child.tag === CoroutineComponent) {
715750
deleteRemainingChildren(returnFiber, child.sibling);
716751
const existing = useFiber(child, priority);
@@ -738,12 +773,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
738773
yieldNode : ReactYield,
739774
priority : PriorityLevel
740775
) : Fiber {
776+
const returnFiberIsFailedErrorBoundary = isFailedErrorBoundary(returnFiber);
741777
const key = yieldNode.key;
742778
let child = currentFirstChild;
743779
while (child) {
744780
// TODO: If key === null and child.key === null, then this only applies to
745781
// the first item in the list.
746-
if (child.key === key) {
782+
if (child.key === key && !returnFiberIsFailedErrorBoundary) {
747783
if (child.tag === YieldComponent) {
748784
deleteRemainingChildren(returnFiber, child.sibling);
749785
const existing = useFiber(child, priority);

src/renderers/shared/fiber/ReactFiberCommitWork.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ module.exports = function<T, P, I, TI, C>(
383383
return {
384384
commitInsertion,
385385
commitDeletion,
386+
commitNestedUnmounts,
386387
commitWork,
387388
commitLifeCycles,
388389
};

src/renderers/shared/fiber/ReactFiberRoot.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
import type { Fiber } from 'ReactFiber';
1616
import type { UpdateQueue } from 'ReactFiberUpdateQueue';
17+
import type { FiberRootErrorPhase } from 'ReactFiberRootErrorPhase';
1718

1819
const { createHostContainerFiber } = require('ReactFiber');
20+
const { NoError } = require('ReactFiberRootErrorPhase');
1921

2022
export type FiberRoot = {
2123
// Any additional information from the host associated with this root.
@@ -24,6 +26,9 @@ export type FiberRoot = {
2426
current: Fiber,
2527
// Determines if this root has already been added to the schedule for work.
2628
isScheduled: boolean,
29+
// When a root fails with an uncaught error, determines the phase of recovery.
30+
// Defaults to NoError
31+
errorPhase: FiberRootErrorPhase,
2732
// The work schedule is a linked list.
2833
nextScheduledRoot: ?FiberRoot,
2934
// Linked list of callbacks to call after updates are committed.
@@ -38,6 +43,7 @@ exports.createFiberRoot = function(containerInfo : any) : FiberRoot {
3843
current: uninitializedFiber,
3944
containerInfo: containerInfo,
4045
isScheduled: false,
46+
errorPhase: NoError,
4147
nextScheduledRoot: null,
4248
callbackList: null,
4349
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 ReactFiberRootErrorPhase
10+
* @flow
11+
*/
12+
13+
'use strict';
14+
15+
export type FiberRootErrorPhase = 0 | 1 | 2;
16+
17+
module.exports = {
18+
NoError: 0,
19+
SoftDeletion: 1,
20+
HardDeletion: 2,
21+
};

0 commit comments

Comments
 (0)