Skip to content

Commit 9baecbf

Browse files
authored
[Fizz] Avoid hanging when suspending after aborting while rendering (#34192)
This fixes an edge case where you abort the render while rendering a component that ends up Suspending. It technically only applied if you were deep enough to be inside `renderNode` and was not susceptible to hanging if the abort + suspending component was being tried inside retryRenderTask/retryReplaytask. The fix is to preempt the thenable checks in renderNode and check if the request is aborting and if so just bubble up to the task handler. The reason this hung before is a new task would get scheduled after we had aborted every other task (minus the currently rendering one). This led to a situation where the task count would not hit zero.
1 parent 0422a00 commit 9baecbf

File tree

2 files changed

+57
-5
lines changed

2 files changed

+57
-5
lines changed

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

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,7 @@ describe('ReactDOMFizzServer', () => {
9494
ReactDOM = require('react-dom');
9595
ReactDOMClient = require('react-dom/client');
9696
ReactDOMFizzServer = require('react-dom/server');
97-
if (__EXPERIMENTAL__) {
98-
ReactDOMFizzStatic = require('react-dom/static');
99-
}
97+
ReactDOMFizzStatic = require('react-dom/static');
10098
Stream = require('stream');
10199
Suspense = React.Suspense;
102100
use = React.use;
@@ -10784,4 +10782,54 @@ Unfortunately that previous paragraph wasn't quite long enough so I'll continue
1078410782
// Instead we assert that we never emitted the fallback of the Suspense boundary around the body.
1078510783
expect(streamedContent).not.toContain(randomTag);
1078610784
});
10785+
10786+
it('should be able to Suspend after aborting in the same component without hanging the render', async () => {
10787+
const controller = new AbortController();
10788+
10789+
const promise1 = new Promise(() => {});
10790+
function AbortAndSuspend() {
10791+
controller.abort('boom');
10792+
return React.use(promise1);
10793+
}
10794+
10795+
function App() {
10796+
return (
10797+
<html>
10798+
<body>
10799+
<Suspense fallback="loading...">
10800+
{/*
10801+
The particular code path that was problematic required the Suspend to happen in renderNode
10802+
rather than retryRenderTask so we render the aborting function inside a host component
10803+
intentionally here
10804+
*/}
10805+
<div>
10806+
<AbortAndSuspend />
10807+
</div>
10808+
</Suspense>
10809+
</body>
10810+
</html>
10811+
);
10812+
}
10813+
10814+
const errors = [];
10815+
await act(async () => {
10816+
const result = await ReactDOMFizzStatic.prerenderToNodeStream(<App />, {
10817+
signal: controller.signal,
10818+
onError(e) {
10819+
errors.push(e);
10820+
},
10821+
});
10822+
10823+
result.prelude.pipe(writable);
10824+
});
10825+
10826+
expect(errors).toEqual(['boom']);
10827+
10828+
expect(getVisibleChildren(document)).toEqual(
10829+
<html>
10830+
<head />
10831+
<body>loading...</body>
10832+
</html>,
10833+
);
10834+
});
1078710835
});

packages/react-server/src/ReactFizzServer.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4155,7 +4155,9 @@ function renderNode(
41554155
getSuspendedThenable()
41564156
: thrownValue;
41574157

4158-
if (typeof x === 'object' && x !== null) {
4158+
if (request.status === ABORTING) {
4159+
// We are aborting so we can just bubble up to the task by falling through
4160+
} else if (typeof x === 'object' && x !== null) {
41594161
// $FlowFixMe[method-unbinding]
41604162
if (typeof x.then === 'function') {
41614163
const wakeable: Wakeable = (x: any);
@@ -4254,7 +4256,9 @@ function renderNode(
42544256
getSuspendedThenable()
42554257
: thrownValue;
42564258

4257-
if (typeof x === 'object' && x !== null) {
4259+
if (request.status === ABORTING) {
4260+
// We are aborting so we can just bubble up to the task by falling through
4261+
} else if (typeof x === 'object' && x !== null) {
42584262
// $FlowFixMe[method-unbinding]
42594263
if (typeof x.then === 'function') {
42604264
const wakeable: Wakeable = (x: any);

0 commit comments

Comments
 (0)