Skip to content

Commit 20d5e2b

Browse files
committed
Regression test: Load stylesheet in error UI
Adds a failing test for a case discovered by Next.js. An error boundary is triggered during initial hydration, and the error fallback includes a stylesheet. If the stylesheet has not yet been loaded, the commit suspends, but never resolves even after the stylesheet finishes loading. Triggering this bug depends on several very specific code paths being triggered simultaneously. There are a few ways we could fix the bug; I'll submit as one or more separate PRs to show that each one is sufficient.
1 parent b277259 commit 20d5e2b

File tree

1 file changed

+87
-0
lines changed

1 file changed

+87
-0
lines changed

packages/react-dom/src/__tests__/ReactDOMFloat-test.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3385,6 +3385,93 @@ body {
33853385
);
33863386
});
33873387

3388+
// @gate FIXME
3389+
it('loading a stylesheet as part of an error boundary UI, during initial render', async () => {
3390+
class ErrorBoundary extends React.Component {
3391+
state = {error: null};
3392+
static getDerivedStateFromError(error) {
3393+
return {error};
3394+
}
3395+
render() {
3396+
const error = this.state.error;
3397+
if (error !== null) {
3398+
return (
3399+
<>
3400+
<link rel="stylesheet" href="A" precedence="default" />
3401+
{error.message}
3402+
</>
3403+
);
3404+
}
3405+
return this.props.children;
3406+
}
3407+
}
3408+
3409+
function Throws() {
3410+
throw new Error('Oops!');
3411+
}
3412+
3413+
function App() {
3414+
return (
3415+
<html>
3416+
<body>
3417+
<ErrorBoundary>
3418+
<Suspense fallback="Loading...">
3419+
<Throws />
3420+
</Suspense>
3421+
</ErrorBoundary>
3422+
</body>
3423+
</html>
3424+
);
3425+
}
3426+
3427+
// Initial server render. Because something threw, a Suspense fallback
3428+
// is shown.
3429+
await act(() => {
3430+
renderToPipeableStream(<App />, {
3431+
onError(x) {
3432+
Scheduler.log('Caught server error: ' + x.message);
3433+
},
3434+
}).pipe(writable);
3435+
});
3436+
expect(getMeaningfulChildren(document)).toEqual(
3437+
<html>
3438+
<head />
3439+
<body>Loading...</body>
3440+
</html>,
3441+
);
3442+
assertLog(['Caught server error: Oops!']);
3443+
3444+
// Hydrate the tree. The error boundary will capture the error and attempt
3445+
// to show an error screen. However, the error screen includes a stylesheet,
3446+
// so the commit should suspend until the stylesheet loads.
3447+
ReactDOMClient.hydrateRoot(document, <App />);
3448+
await waitForAll([]);
3449+
3450+
// A preload for the stylesheet is inserted, but we still haven't committed
3451+
// the error screen.
3452+
expect(getMeaningfulChildren(document)).toEqual(
3453+
<html>
3454+
<head>
3455+
<link as="style" href="A" rel="preload" />
3456+
</head>
3457+
<body>Loading...</body>
3458+
</html>,
3459+
);
3460+
3461+
// Finish loading the stylesheets. The commit should be unblocked, and the
3462+
// error screen should appear.
3463+
await clientAct(() => loadStylesheets());
3464+
expect(getMeaningfulChildren(document)).toEqual(
3465+
<html>
3466+
<head>
3467+
<link data-precedence="default" href="A" rel="stylesheet" />
3468+
<link as="style" href="A" rel="preload" />
3469+
</head>
3470+
<body>Oops!</body>
3471+
</html>,
3472+
);
3473+
});
3474+
33883475
it('will not flush a preload for a new rendered Stylesheet Resource if one was already flushed', async () => {
33893476
function Component() {
33903477
ReactDOM.preload('foo', {as: 'style'});

0 commit comments

Comments
 (0)