mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
Fix: Errors should not escape a hidden Activity (#35074)
If an error is thrown inside a hidden Activity, it should not escape into the visible part of the UI. Conceptually, a hidden Activity boundary is not part of the current UI; it's the same as an unmounted tree, except for the fact that the state will be restored if it's later revealed. Fixes: - https://github.com/facebook/react/issues/35073
This commit is contained in:
20
packages/react-reconciler/src/ReactFiberThrow.js
vendored
20
packages/react-reconciler/src/ReactFiberThrow.js
vendored
@@ -12,7 +12,10 @@ import type {Lane, Lanes} from './ReactFiberLane';
|
||||
import type {CapturedValue} from './ReactCapturedValue';
|
||||
import type {Update} from './ReactFiberClassUpdateQueue';
|
||||
import type {Wakeable} from 'shared/ReactTypes';
|
||||
import type {OffscreenQueue} from './ReactFiberOffscreenComponent';
|
||||
import type {
|
||||
OffscreenQueue,
|
||||
OffscreenState,
|
||||
} from './ReactFiberOffscreenComponent';
|
||||
import type {RetryQueue} from './ReactFiberSuspenseComponent';
|
||||
|
||||
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
||||
@@ -676,6 +679,21 @@ function throwException(
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OffscreenComponent: {
|
||||
const offscreenState: OffscreenState | null =
|
||||
(workInProgress.memoizedState: any);
|
||||
if (offscreenState !== null) {
|
||||
// An error was thrown inside a hidden Offscreen boundary. This should
|
||||
// not be allowed to escape into the visible part of the UI. Mark the
|
||||
// boundary with ShouldCapture to abort the ongoing prerendering
|
||||
// attempt. This is the same flag would be set if something were to
|
||||
// suspend. It will be cleared the next time the boundary
|
||||
// is attempted.
|
||||
workInProgress.flags |= ShouldCapture;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
99
packages/react-reconciler/src/__tests__/ActivityErrorHandling-test.js
vendored
Normal file
99
packages/react-reconciler/src/__tests__/ActivityErrorHandling-test.js
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
let React;
|
||||
let ReactNoop;
|
||||
let Scheduler;
|
||||
let act;
|
||||
let Activity;
|
||||
let useState;
|
||||
let assertLog;
|
||||
|
||||
describe('Activity error handling', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
Scheduler = require('scheduler');
|
||||
act = require('internal-test-utils').act;
|
||||
Activity = React.Activity;
|
||||
useState = React.useState;
|
||||
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
assertLog = InternalTestUtils.assertLog;
|
||||
});
|
||||
|
||||
function Text({text}) {
|
||||
Scheduler.log(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
// @gate enableActivity
|
||||
it(
|
||||
'errors inside a hidden Activity do not escape in the visible part ' +
|
||||
'of the UI',
|
||||
async () => {
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {error: null};
|
||||
static getDerivedStateFromError(error) {
|
||||
return {error};
|
||||
}
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<Text text={`Caught an error: ${this.state.error.message}`} />
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function Throws() {
|
||||
throw new Error('Oops!');
|
||||
}
|
||||
|
||||
let setShowMore;
|
||||
function App({content, more}) {
|
||||
const [showMore, _setShowMore] = useState(false);
|
||||
setShowMore = _setShowMore;
|
||||
return (
|
||||
<>
|
||||
<div>{content}</div>
|
||||
<div>
|
||||
<ErrorBoundary>
|
||||
<Activity mode={showMore ? 'visible' : 'hidden'}>
|
||||
{more}
|
||||
</Activity>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
await act(() =>
|
||||
ReactNoop.render(
|
||||
<App content={<Text text="Visible" />} more={<Throws />} />,
|
||||
),
|
||||
);
|
||||
|
||||
// Initial render. An error is thrown when prerendering the hidden
|
||||
// Activity boundary, but since it's hidden, the UI doesn't observe it.
|
||||
assertLog(['Visible']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Visible</div>
|
||||
<div />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Once the Activity boundary is revealed, the error is thrown and
|
||||
// captured by the outer ErrorBoundary.
|
||||
await act(() => setShowMore(true));
|
||||
assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Visible</div>
|
||||
<div>Caught an error: Oops!</div>
|
||||
</>,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -403,7 +403,7 @@ describe('ReactFreshIntegration', () => {
|
||||
await patch(code);
|
||||
});
|
||||
|
||||
// @gate __DEV__ && enableActivity && enableScopeAPI
|
||||
// @gate __DEV__ && enableActivity
|
||||
it('ignores ref for Scope in hidden subtree', async () => {
|
||||
const code = `
|
||||
import {
|
||||
|
||||
Reference in New Issue
Block a user