[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.
This commit is contained in:
Josh Story
2025-08-12 16:46:56 -07:00
committed by GitHub
parent 0422a00e3e
commit 9baecbf02b
2 changed files with 57 additions and 5 deletions

View File

@@ -94,9 +94,7 @@ describe('ReactDOMFizzServer', () => {
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
ReactDOMFizzServer = require('react-dom/server');
if (__EXPERIMENTAL__) {
ReactDOMFizzStatic = require('react-dom/static');
}
ReactDOMFizzStatic = require('react-dom/static');
Stream = require('stream');
Suspense = React.Suspense;
use = React.use;
@@ -10784,4 +10782,54 @@ Unfortunately that previous paragraph wasn't quite long enough so I'll continue
// Instead we assert that we never emitted the fallback of the Suspense boundary around the body.
expect(streamedContent).not.toContain(randomTag);
});
it('should be able to Suspend after aborting in the same component without hanging the render', async () => {
const controller = new AbortController();
const promise1 = new Promise(() => {});
function AbortAndSuspend() {
controller.abort('boom');
return React.use(promise1);
}
function App() {
return (
<html>
<body>
<Suspense fallback="loading...">
{/*
The particular code path that was problematic required the Suspend to happen in renderNode
rather than retryRenderTask so we render the aborting function inside a host component
intentionally here
*/}
<div>
<AbortAndSuspend />
</div>
</Suspense>
</body>
</html>
);
}
const errors = [];
await act(async () => {
const result = await ReactDOMFizzStatic.prerenderToNodeStream(<App />, {
signal: controller.signal,
onError(e) {
errors.push(e);
},
});
result.prelude.pipe(writable);
});
expect(errors).toEqual(['boom']);
expect(getVisibleChildren(document)).toEqual(
<html>
<head />
<body>loading...</body>
</html>,
);
});
});

View File

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