Skip to content

Commit 2508888

Browse files
committed
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.
1 parent 7e82214 commit 2508888

File tree

3 files changed

+82
-19
lines changed

3 files changed

+82
-19
lines changed

scripts/fiber/tests-passing.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,7 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
11191119
* catches reconciler errors in a boundary during update
11201120
* recovers from uncaught reconciler errors
11211121
* unmounts components with uncaught errors
1122+
* does not interrupt unmounting if detaching a ref throws
11221123

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

src/renderers/shared/fiber/ReactFiberCommitWork.js

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,28 @@ module.exports = function<T, P, I, TI, C>(
4646
const insertBefore = config.insertBefore;
4747
const removeChild = config.removeChild;
4848

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);
5367
}
5468
}
5569

70+
// Only called during update. It's ok to throw.
5671
function detachRefIfNeeded(current : ?Fiber, finishedWork : Fiber) {
5772
if (current) {
5873
const currentRef = current.ref;
@@ -298,21 +313,21 @@ module.exports = function<T, P, I, TI, C>(
298313
}
299314
}
300315

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
301319
function commitUnmount(current : Fiber) : void {
302320
switch (current.tag) {
303321
case ClassComponent: {
304-
detachRef(current);
322+
safelyDetachRef(current);
305323
const instance = current.stateNode;
306324
if (typeof instance.componentWillUnmount === 'function') {
307-
const error = tryCallComponentWillUnmount(instance);
308-
if (error) {
309-
captureError(current, error);
310-
}
325+
safelyCallComponentWillUnmount(current, instance);
311326
}
312327
return;
313328
}
314329
case HostComponent: {
315-
detachRef(current);
330+
safelyDetachRef(current);
316331
return;
317332
}
318333
case CoroutineComponent: {
@@ -423,15 +438,6 @@ module.exports = function<T, P, I, TI, C>(
423438
}
424439
}
425440

426-
function tryCallComponentWillUnmount(instance) {
427-
try {
428-
instance.componentWillUnmount();
429-
return null;
430-
} catch (error) {
431-
return error;
432-
}
433-
}
434-
435441
return {
436442
commitPlacement,
437443
commitDeletion,

src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ describe('ReactIncrementalErrorHandling', () => {
2121
ReactNoop = require('ReactNoop');
2222
});
2323

24+
function div(...children) {
25+
children = children.map(c => typeof c === 'string' ? { text: c } : c);
26+
return { type: 'div', children, prop: undefined };
27+
}
28+
2429
function span(prop) {
2530
return { type: 'span', children: [], prop };
2631
}
@@ -860,4 +865,55 @@ describe('ReactIncrementalErrorHandling', () => {
860865
]);
861866
expect(ReactNoop.getChildren()).toEqual([]);
862867
});
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+
});
863919
});

0 commit comments

Comments
 (0)