@samitouri / QOS-React-2 / commits / 2508888a2b

Errors thrown when detaching a ref should not interrupt unmount

If a ref throws while detaching, it should not prevent componentWillUnmount from being called. Additionally, errors thrown by removeChild should not be ignored, even as the result of unmounting a failed subtree.

Andrew Clark committed Dec 6, 2016 at 13:55 UTC 2508888a2b2273b10c39a5a76818f5e58d0248a9
3 files changed +82 -19
scripts/fiber/tests-passing.txt
+1
@@ -1119,6 +1119,7 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
1119 * catches reconciler errors in a boundary during update
1120 * recovers from uncaught reconciler errors
1121 * unmounts components with uncaught errors
1122 +* does not interrupt unmounting if detaching a ref throws
1123
1124 src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js
1125 * handles isMounted even when the initial render is deferred
src/renderers/shared/fiber/ReactFiberCommitWork.js
+25 -19
@@ -46,13 +46,28 @@ module.exports = function<T, P, I, TI, C>(
46 const insertBefore = config.insertBefore;
47 const removeChild = config.removeChild;
48
49 - function detachRef(current : Fiber) {
50 - const ref = current.ref;
51 - if (ref) {
52 - ref(null);
49 + // Capture errors so they don't interrupt unmounting.
50 + function safelyCallComponentWillUnmount(current, instance) {
51 + try {
52 + instance.componentWillUnmount();
53 + } catch (error) {
54 + captureError(current, error);
55 + }
56 + }
57 +
58 + // Capture errors so they don't interrupt unmounting.
59 + function safelyDetachRef(current : Fiber) {
60 + try {
61 + const ref = current.ref;
62 + if (ref) {
63 + ref(null);
64 + }
65 + } catch (error) {
66 + captureError(current, error);
67 }
68 }
69
70 + // Only called during update. It's ok to throw.
71 function detachRefIfNeeded(current : ?Fiber, finishedWork : Fiber) {
72 if (current) {
73 const currentRef = current.ref;
@@ -298,21 +313,21 @@ module.exports = function<T, P, I, TI, C>(
313 }
314 }
315
316 + // User-originating errors (lifecycles and refs) should not interrupt
317 + // deletion, so don't let them throw. Host-originating errors should
318 + // interrupt deletion, so it's okay
319 function commitUnmount(current : Fiber) : void {
320 switch (current.tag) {
321 case ClassComponent: {
304 - detachRef(current);
322 + safelyDetachRef(current);
323 const instance = current.stateNode;
324 if (typeof instance.componentWillUnmount === 'function') {
307 - const error = tryCallComponentWillUnmount(instance);
308 - if (error) {
309 - captureError(current, error);
310 - }
325 + safelyCallComponentWillUnmount(current, instance);
326 }
327 return;
328 }
329 case HostComponent: {
315 - detachRef(current);
330 + safelyDetachRef(current);
331 return;
332 }
333 case CoroutineComponent: {
@@ -423,15 +438,6 @@ module.exports = function<T, P, I, TI, C>(
438 }
439 }
440
426 - function tryCallComponentWillUnmount(instance) {
427 - try {
428 - instance.componentWillUnmount();
429 - return null;
430 - } catch (error) {
431 - return error;
432 - }
433 - }
434 -
441 return {
442 commitPlacement,
443 commitDeletion,
src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
+56
@@ -21,6 +21,11 @@ describe('ReactIncrementalErrorHandling', () => {
21 ReactNoop = require('ReactNoop');
22 });
23
24 + function div(...children) {
25 + children = children.map(c => typeof c === 'string' ? { text: c } : c);
26 + return { type: 'div', children, prop: undefined };
27 + }
28 +
29 function span(prop) {
30 return { type: 'span', children: [], prop };
31 }
@@ -860,4 +865,55 @@ describe('ReactIncrementalErrorHandling', () => {
865 ]);
866 expect(ReactNoop.getChildren()).toEqual([]);
867 });
868 +
869 + it('does not interrupt unmounting if detaching a ref throws', () => {
870 + var ops = [];
871 +
872 + class Bar extends React.Component {
873 + componentWillUnmount() {
874 + ops.push('Bar unmount');
875 + }
876 + render() {
877 + return <span prop="Bar" />;
878 + }
879 + }
880 +
881 + function barRef(inst) {
882 + if (inst === null) {
883 + ops.push('barRef detach');
884 + throw new Error('Detach error');
885 + }
886 + ops.push('barRef attach');
887 + }
888 +
889 + function Foo(props) {
890 + return (
891 + <div>
892 + {props.hide ? null : <Bar ref={barRef} />}
893 + </div>
894 + );
895 + }
896 +
897 + ReactNoop.render(<Foo />);
898 + ReactNoop.flush();
899 + expect(ops).toEqual(['barRef attach']);
900 + expect(ReactNoop.getChildren()).toEqual([
901 + div(
902 + span('Bar'),
903 + ),
904 + ]);
905 +
906 + ops = [];
907 +
908 + // Unmount
909 + ReactNoop.render(<Foo hide={true} />);
910 + expect(() => ReactNoop.flush()).toThrow('Detach error');
911 + expect(ops).toEqual([
912 + 'barRef detach',
913 + // Bar should unmount even though its ref threw an error while detaching
914 + 'Bar unmount',
915 + ]);
916 + // Because there was an error, entire tree should unmount
917 + expect(ReactNoop.getChildren()).toEqual([]);
918 + });
919 });