Fix bugs related to unmounting error boundaries (#10403)

* Don't warn about setState on unmounted when scheduling error recovery

We shouldn't schedule an update on unmounted error boundaries, but we
don't know if a boundary is unmounted until we traverse its parents.
Added an additional argument to scheduleUpdate so we know not to warn
about setState on unmounted components.

* Should be able to unmount an error boundary before it is handled

Fixes the case where an error boundary captures an error, but its
parent is unmounted before we can re-render it. componentDidCatch is
never called, and we don't remove the boundary from our set of
unhandled error boundaries.

We should not assume that if capturedErrors is non-null that we still
have unhandled errors.
This commit is contained in:
Andrew Clark authored and GitHub committed 2017-08-08 10:09:43 -07:00
1 parent 9ebd0c9e9a
commit 5cdd744e06
2 files changed
+121 -23

No files matched your search

@@ -761,7 +761,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
// The loop stops once the children have unmounted and error lifecycles are
// called. Then we return to the regular flow.
if (capturedErrors !== null && capturedErrors.size > 0) {
if (
capturedErrors !== null &&
capturedErrors.size > 0 &&
nextPriorityLevel === TaskPriority
) {
while (nextUnitOfWork !== null) {
if (hasCapturedError(nextUnitOfWork)) {
// Use a forked version of performUnitOfWork
@@ -780,19 +784,17 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
commitAllWork(pendingCommit);
priorityContext = nextPriorityLevel;
if (capturedErrors === null || capturedErrors.size === 0) {
if (
capturedErrors === null ||
capturedErrors.size === 0 ||
nextPriorityLevel !== TaskPriority
) {
// There are no more unhandled errors. We can exit this special
// work loop. If there's still additional work, we'll perform it
// using one of the normal work loops.
break;
}
// The commit phase produced additional errors. Continue working.
invariant(
nextPriorityLevel === TaskPriority,
'Commit phase errors should be scheduled to recover with task ' +
'priority. This error is likely caused by a bug in React. ' +
'Please file an issue.',
);
}
}
}
@@ -1129,7 +1131,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
willRetry = true;
}
} else if (node.tag === HostRoot) {
// Treat the root like a no-op error boundary.
// Treat the root like a no-op error boundary
boundary = node;
}
@@ -1349,6 +1351,14 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
}
function scheduleUpdate(fiber: Fiber, priorityLevel: PriorityLevel) {
return scheduleUpdateImpl(fiber, priorityLevel, false);
}
function scheduleUpdateImpl(
fiber: Fiber,
priorityLevel: PriorityLevel,
isErrorRecovery: boolean,
) {
if (__DEV__) {
recordScheduleUpdate();
}
@@ -1372,7 +1382,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
}
if (__DEV__) {
if (fiber.tag === ClassComponent) {
if (!isErrorRecovery && fiber.tag === ClassComponent) {
const instance = fiber.stateNode;
warnAboutInvalidUpdates(instance);
}
@@ -1438,7 +1448,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
}
} else {
if (__DEV__) {
if (fiber.tag === ClassComponent) {
if (!isErrorRecovery && fiber.tag === ClassComponent) {
warnAboutUpdateOnUnmounted(fiber.stateNode);
}
}
@@ -1478,7 +1488,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
}
function scheduleErrorRecovery(fiber: Fiber) {
scheduleUpdate(fiber, TaskPriority);
scheduleUpdateImpl(fiber, TaskPriority, true);
}
function performWithPriority(priorityLevel: PriorityLevel, fn: Function) {
@@ -177,11 +177,7 @@ describe('ReactIncrementalErrorHandling', () => {
}
ReactNoop.flushSync(() => {
ReactNoop.render(
<ErrorBoundary>
Before the storm.
</ErrorBoundary>,
);
ReactNoop.render(<ErrorBoundary>Before the storm.</ErrorBoundary>);
ReactNoop.render(
<ErrorBoundary>
<BrokenRender />
@@ -327,9 +323,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(() => {
ReactNoop.flushSync(() => {
ReactNoop.render(
<RethrowErrorBoundary>
Before the storm.
</RethrowErrorBoundary>,
<RethrowErrorBoundary>Before the storm.</RethrowErrorBoundary>,
);
ReactNoop.render(
<RethrowErrorBoundary>
@@ -478,6 +472,82 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ops).toEqual(['Foo']);
});
it('should not attempt to recover an unmounting error boundary', () => {
class Parent extends React.Component {
componentWillUnmount() {
ReactNoop.yield('Parent componentWillUnmount');
}
render() {
return <Boundary />;
}
}
class Boundary extends React.Component {
componentDidCatch(e) {
ReactNoop.yield(`Caught error: ${e.message}`);
}
render() {
return <ThrowsOnUnmount />;
}
}
class ThrowsOnUnmount extends React.Component {
componentWillUnmount() {
ReactNoop.yield('ThrowsOnUnmount componentWillUnmount');
throw new Error('unmount error');
}
render() {
return null;
}
}
ReactNoop.render(<Parent />);
ReactNoop.flush();
ReactNoop.render(null);
expect(ReactNoop.flush()).toEqual([
// Parent unmounts before the error is thrown.
'Parent componentWillUnmount',
'ThrowsOnUnmount componentWillUnmount',
]);
ReactNoop.render(<Parent />);
});
it('can unmount an error boundary before it is handled', () => {
let parent;
class Parent extends React.Component {
state = {step: 0};
render() {
parent = this;
return this.state.step === 0 ? <Boundary /> : null;
}
}
class Boundary extends React.Component {
componentDidCatch() {}
render() {
return <Child />;
}
}
class Child extends React.Component {
componentDidUpdate() {
parent.setState({step: 1});
throw new Error('update error');
}
render() {
return null;
}
}
ReactNoop.render(<Parent />);
ReactNoop.flush();
ReactNoop.flushSync(() => {
ReactNoop.render(<Parent />);
});
});
it('continues work on other roots despite caught errors', () => {
class ErrorBoundary extends React.Component {
state = {error: null};
@@ -896,7 +966,13 @@ describe('ReactIncrementalErrorHandling', () => {
}
try {
ReactNoop.render(<div><span><ErrorThrowingComponent /></span></div>);
ReactNoop.render(
<div>
<span>
<ErrorThrowingComponent />
</span>
</div>,
);
ReactNoop.flushDeferredPri();
} catch (error) {}
@@ -927,7 +1003,13 @@ describe('ReactIncrementalErrorHandling', () => {
}
try {
ReactNoop.render(<div><span><ErrorThrowingComponent /></span></div>);
ReactNoop.render(
<div>
<span>
<ErrorThrowingComponent />
</span>
</div>,
);
ReactNoop.flushDeferredPri();
} catch (error) {}
@@ -965,7 +1047,13 @@ describe('ReactIncrementalErrorHandling', () => {
);
try {
ReactNoop.render(<div><span><ErrorThrowingComponent /></span></div>);
ReactNoop.render(
<div>
<span>
<ErrorThrowingComponent />
</span>
</div>,
);
ReactNoop.flushDeferredPri();
} catch (error) {}