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
});