Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/react-devtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@
"internal-ip": "^6.2.0",
"minimist": "^1.2.3",
"react-devtools-core": "7.0.1",
"update-notifier": "^2.1.0"
"update-notifier": "^5.0.0"
}
}
20 changes: 19 additions & 1 deletion packages/react-reconciler/src/ReactFiberThrow.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
</>,
);
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading