@samitouri / QOS-React / commits / 5cdd744e06

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.

Andrew Clark committed Aug 8, 2017 at 10:09 UTC 5cdd744e0610c914aa34ec1eb5a566006d693cae
2 files changed +121 -23
src/renderers/shared/fiber/ReactFiberScheduler.js
+22 -12
@@ -761,7 +761,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
761 // The loop stops once the children have unmounted and error lifecycles are
762 // called. Then we return to the regular flow.
763
764 - if (capturedErrors !== null && capturedErrors.size > 0) {
764 + if (
765 + capturedErrors !== null &&
766 + capturedErrors.size > 0 &&
767 + nextPriorityLevel === TaskPriority
768 + ) {
769 while (nextUnitOfWork !== null) {
770 if (hasCapturedError(nextUnitOfWork)) {
771 // Use a forked version of performUnitOfWork
@@ -780,19 +784,17 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
784 commitAllWork(pendingCommit);
785 priorityContext = nextPriorityLevel;
786
783 - if (capturedErrors === null || capturedErrors.size === 0) {
787 + if (
788 + capturedErrors === null ||
789 + capturedErrors.size === 0 ||
790 + nextPriorityLevel !== TaskPriority
791 + ) {
792 // There are no more unhandled errors. We can exit this special
793 // work loop. If there's still additional work, we'll perform it
794 // using one of the normal work loops.
795 break;
796 }
797 // The commit phase produced additional errors. Continue working.
790 - invariant(
791 - nextPriorityLevel === TaskPriority,
792 - 'Commit phase errors should be scheduled to recover with task ' +
793 - 'priority. This error is likely caused by a bug in React. ' +
794 - 'Please file an issue.',
795 - );
798 }
799 }
800 }
@@ -1129,7 +1131,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1131 willRetry = true;
1132 }
1133 } else if (node.tag === HostRoot) {
1132 - // Treat the root like a no-op error boundary.
1134 + // Treat the root like a no-op error boundary
1135 boundary = node;
1136 }
1137
@@ -1349,6 +1351,14 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1351 }
1352
1353 function scheduleUpdate(fiber: Fiber, priorityLevel: PriorityLevel) {
1354 + return scheduleUpdateImpl(fiber, priorityLevel, false);
1355 + }
1356 +
1357 + function scheduleUpdateImpl(
1358 + fiber: Fiber,
1359 + priorityLevel: PriorityLevel,
1360 + isErrorRecovery: boolean,
1361 + ) {
1362 if (__DEV__) {
1363 recordScheduleUpdate();
1364 }
@@ -1372,7 +1382,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1382 }
1383
1384 if (__DEV__) {
1375 - if (fiber.tag === ClassComponent) {
1385 + if (!isErrorRecovery && fiber.tag === ClassComponent) {
1386 const instance = fiber.stateNode;
1387 warnAboutInvalidUpdates(instance);
1388 }
@@ -1438,7 +1448,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1448 }
1449 } else {
1450 if (__DEV__) {
1441 - if (fiber.tag === ClassComponent) {
1451 + if (!isErrorRecovery && fiber.tag === ClassComponent) {
1452 warnAboutUpdateOnUnmounted(fiber.stateNode);
1453 }
1454 }
@@ -1478,7 +1488,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1488 }
1489
1490 function scheduleErrorRecovery(fiber: Fiber) {
1481 - scheduleUpdate(fiber, TaskPriority);
1491 + scheduleUpdateImpl(fiber, TaskPriority, true);
1492 }
1493
1494 function performWithPriority(priorityLevel: PriorityLevel, fn: Function) {
src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
+99 -11
@@ -177,11 +177,7 @@ describe('ReactIncrementalErrorHandling', () => {
177 }
178
179 ReactNoop.flushSync(() => {
180 - ReactNoop.render(
181 - <ErrorBoundary>
182 - Before the storm.
183 - </ErrorBoundary>,
184 - );
180 + ReactNoop.render(<ErrorBoundary>Before the storm.</ErrorBoundary>);
181 ReactNoop.render(
182 <ErrorBoundary>
183 <BrokenRender />
@@ -327,9 +323,7 @@ describe('ReactIncrementalErrorHandling', () => {
323 expect(() => {
324 ReactNoop.flushSync(() => {
325 ReactNoop.render(
330 - <RethrowErrorBoundary>
331 - Before the storm.
332 - </RethrowErrorBoundary>,
326 + <RethrowErrorBoundary>Before the storm.</RethrowErrorBoundary>,
327 );
328 ReactNoop.render(
329 <RethrowErrorBoundary>
@@ -478,6 +472,82 @@ describe('ReactIncrementalErrorHandling', () => {
472 expect(ops).toEqual(['Foo']);
473 });
474
475 + it('should not attempt to recover an unmounting error boundary', () => {
476 + class Parent extends React.Component {
477 + componentWillUnmount() {
478 + ReactNoop.yield('Parent componentWillUnmount');
479 + }
480 + render() {
481 + return <Boundary />;
482 + }
483 + }
484 +
485 + class Boundary extends React.Component {
486 + componentDidCatch(e) {
487 + ReactNoop.yield(`Caught error: ${e.message}`);
488 + }
489 + render() {
490 + return <ThrowsOnUnmount />;
491 + }
492 + }
493 +
494 + class ThrowsOnUnmount extends React.Component {
495 + componentWillUnmount() {
496 + ReactNoop.yield('ThrowsOnUnmount componentWillUnmount');
497 + throw new Error('unmount error');
498 + }
499 + render() {
500 + return null;
501 + }
502 + }
503 +
504 + ReactNoop.render(<Parent />);
505 + ReactNoop.flush();
506 + ReactNoop.render(null);
507 + expect(ReactNoop.flush()).toEqual([
508 + // Parent unmounts before the error is thrown.
509 + 'Parent componentWillUnmount',
510 + 'ThrowsOnUnmount componentWillUnmount',
511 + ]);
512 + ReactNoop.render(<Parent />);
513 + });
514 +
515 + it('can unmount an error boundary before it is handled', () => {
516 + let parent;
517 +
518 + class Parent extends React.Component {
519 + state = {step: 0};
520 + render() {
521 + parent = this;
522 + return this.state.step === 0 ? <Boundary /> : null;
523 + }
524 + }
525 +
526 + class Boundary extends React.Component {
527 + componentDidCatch() {}
528 + render() {
529 + return <Child />;
530 + }
531 + }
532 +
533 + class Child extends React.Component {
534 + componentDidUpdate() {
535 + parent.setState({step: 1});
536 + throw new Error('update error');
537 + }
538 + render() {
539 + return null;
540 + }
541 + }
542 +
543 + ReactNoop.render(<Parent />);
544 + ReactNoop.flush();
545 +
546 + ReactNoop.flushSync(() => {
547 + ReactNoop.render(<Parent />);
548 + });
549 + });
550 +
551 it('continues work on other roots despite caught errors', () => {
552 class ErrorBoundary extends React.Component {
553 state = {error: null};
@@ -896,7 +966,13 @@ describe('ReactIncrementalErrorHandling', () => {
966 }
967
968 try {
899 - ReactNoop.render(<div><span><ErrorThrowingComponent /></span></div>);
969 + ReactNoop.render(
970 + <div>
971 + <span>
972 + <ErrorThrowingComponent />
973 + </span>
974 + </div>,
975 + );
976 ReactNoop.flushDeferredPri();
977 } catch (error) {}
978
@@ -927,7 +1003,13 @@ describe('ReactIncrementalErrorHandling', () => {
1003 }
1004
1005 try {
930 - ReactNoop.render(<div><span><ErrorThrowingComponent /></span></div>);
1006 + ReactNoop.render(
1007 + <div>
1008 + <span>
1009 + <ErrorThrowingComponent />
1010 + </span>
1011 + </div>,
1012 + );
1013 ReactNoop.flushDeferredPri();
1014 } catch (error) {}
1015
@@ -965,7 +1047,13 @@ describe('ReactIncrementalErrorHandling', () => {
1047 );
1048
1049 try {
968 - ReactNoop.render(<div><span><ErrorThrowingComponent /></span></div>);
1050 + ReactNoop.render(
1051 + <div>
1052 + <span>
1053 + <ErrorThrowingComponent />
1054 + </span>
1055 + </div>,
1056 + );
1057 ReactNoop.flushDeferredPri();
1058 } catch (error) {}
1059