mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Fix runtime error that happens if a passive destroy function throws within an unmounted tree (#19543)
A passive effect's cleanup function may throw after an unmount. In that event, React sometimes threw an uncaught runtime error trying to access a property on a null stateNode field. This commit fixes that (and adds a regression test).
This commit is contained in:
@@ -1011,6 +1011,10 @@ function detachFiberMutation(fiber: Fiber) {
|
||||
// with findDOMNode and how it requires the sibling field to carry out
|
||||
// traversal in a later effect. See PR #16820. We now clear the sibling
|
||||
// field after effects, see: detachFiberAfterEffects.
|
||||
//
|
||||
// Don't disconnect stateNode now; it will be detached in detachFiberAfterEffects.
|
||||
// It may be required if the current component is an error boundary,
|
||||
// and one of its descendants throws while unmounting a passive effect.
|
||||
fiber.alternate = null;
|
||||
fiber.child = null;
|
||||
fiber.dependencies = null;
|
||||
@@ -1020,7 +1024,6 @@ function detachFiberMutation(fiber: Fiber) {
|
||||
fiber.memoizedState = null;
|
||||
fiber.pendingProps = null;
|
||||
fiber.return = null;
|
||||
fiber.stateNode = null;
|
||||
fiber.updateQueue = null;
|
||||
if (__DEV__) {
|
||||
fiber._debugOwner = null;
|
||||
|
||||
@@ -3793,4 +3793,5 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
|
||||
|
||||
function detachFiberAfterEffects(fiber: Fiber): void {
|
||||
fiber.sibling = null;
|
||||
fiber.stateNode = null;
|
||||
}
|
||||
|
||||
@@ -2316,6 +2316,106 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Unmount: 1']);
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
});
|
||||
|
||||
describe('errors thrown in passive destroy function within unmounted trees', () => {
|
||||
let BrokenUseEffectCleanup;
|
||||
let ErrorBoundary;
|
||||
let LogOnlyErrorBoundary;
|
||||
|
||||
beforeEach(() => {
|
||||
BrokenUseEffectCleanup = function() {
|
||||
useEffect(() => {
|
||||
Scheduler.unstable_yieldValue('BrokenUseEffectCleanup useEffect');
|
||||
return () => {
|
||||
Scheduler.unstable_yieldValue(
|
||||
'BrokenUseEffectCleanup useEffect destroy',
|
||||
);
|
||||
throw new Error('Expected error');
|
||||
};
|
||||
}, []);
|
||||
|
||||
return 'inner child';
|
||||
};
|
||||
|
||||
ErrorBoundary = class extends React.Component {
|
||||
state = {error: null};
|
||||
static getDerivedStateFromError(error) {
|
||||
Scheduler.unstable_yieldValue(
|
||||
`ErrorBoundary static getDerivedStateFromError`,
|
||||
);
|
||||
return {error};
|
||||
}
|
||||
componentDidCatch(error, info) {
|
||||
Scheduler.unstable_yieldValue(`ErrorBoundary componentDidCatch`);
|
||||
}
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
Scheduler.unstable_yieldValue('ErrorBoundary render error');
|
||||
return 'ErrorBoundary fallback';
|
||||
}
|
||||
Scheduler.unstable_yieldValue('ErrorBoundary render success');
|
||||
return this.props.children;
|
||||
}
|
||||
};
|
||||
|
||||
LogOnlyErrorBoundary = class extends React.Component {
|
||||
componentDidCatch(error, info) {
|
||||
Scheduler.unstable_yieldValue(
|
||||
`LogOnlyErrorBoundary componentDidCatch`,
|
||||
);
|
||||
}
|
||||
render() {
|
||||
Scheduler.unstable_yieldValue(`LogOnlyErrorBoundary render`);
|
||||
return this.props.children;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should not error if the nearest unmounted boundary is log-only', () => {
|
||||
function Conditional({showChildren}) {
|
||||
if (showChildren) {
|
||||
return (
|
||||
<LogOnlyErrorBoundary>
|
||||
<BrokenUseEffectCleanup />
|
||||
</LogOnlyErrorBoundary>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
act(() => {
|
||||
ReactNoop.render(
|
||||
<ErrorBoundary>
|
||||
<Conditional showChildren={true} />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'ErrorBoundary render success',
|
||||
'LogOnlyErrorBoundary render',
|
||||
'BrokenUseEffectCleanup useEffect',
|
||||
]);
|
||||
|
||||
act(() => {
|
||||
ReactNoop.render(
|
||||
<ErrorBoundary>
|
||||
<Conditional showChildren={false} />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'ErrorBoundary render success',
|
||||
]);
|
||||
});
|
||||
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'BrokenUseEffectCleanup useEffect destroy',
|
||||
// This should call componentDidCatch too, but we'll address that in a follow up.
|
||||
// 'LogOnlyErrorBoundary componentDidCatch',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useLayoutEffect', () => {
|
||||
|
||||
Reference in New Issue
Block a user