mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
[Fizz] Clean up row that was blocked by an aborted boundary (#33318)
Fixes a bug that we caused us to hang after an abort because we didn't manage the ref count correctly.
This commit is contained in:
parent
50389e1792
commit
9c7b10e22e
|
|
@ -27,9 +27,10 @@ let writable;
|
|||
let container;
|
||||
let buffer = '';
|
||||
let hasErrored = false;
|
||||
let hasCompleted = false;
|
||||
let fatalError = undefined;
|
||||
|
||||
describe('ReactDOMFizSuspenseList', () => {
|
||||
describe('ReactDOMFizzSuspenseList', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
JSDOM = require('jsdom').JSDOM;
|
||||
|
|
@ -59,6 +60,7 @@ describe('ReactDOMFizSuspenseList', () => {
|
|||
|
||||
buffer = '';
|
||||
hasErrored = false;
|
||||
hasCompleted = false;
|
||||
|
||||
writable = new Stream.PassThrough();
|
||||
writable.setEncoding('utf8');
|
||||
|
|
@ -69,6 +71,9 @@ describe('ReactDOMFizSuspenseList', () => {
|
|||
hasErrored = true;
|
||||
fatalError = error;
|
||||
});
|
||||
writable.on('finish', () => {
|
||||
hasCompleted = true;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -103,7 +108,12 @@ describe('ReactDOMFizSuspenseList', () => {
|
|||
|
||||
function createAsyncText(text) {
|
||||
let resolved = false;
|
||||
let error = undefined;
|
||||
const Component = function () {
|
||||
if (error !== undefined) {
|
||||
Scheduler.log('Error! [' + error.message + ']');
|
||||
throw error;
|
||||
}
|
||||
if (!resolved) {
|
||||
Scheduler.log('Suspend! [' + text + ']');
|
||||
throw promise;
|
||||
|
|
@ -115,6 +125,10 @@ describe('ReactDOMFizSuspenseList', () => {
|
|||
resolved = true;
|
||||
return resolve();
|
||||
};
|
||||
Component.reject = function (e) {
|
||||
error = e;
|
||||
return resolve();
|
||||
};
|
||||
});
|
||||
return Component;
|
||||
}
|
||||
|
|
@ -714,4 +728,120 @@ describe('ReactDOMFizSuspenseList', () => {
|
|||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableSuspenseList
|
||||
it('can abort a pending SuspenseList', async () => {
|
||||
const A = createAsyncText('A');
|
||||
|
||||
function Foo() {
|
||||
return (
|
||||
<div>
|
||||
<SuspenseList revealOrder="forwards">
|
||||
<Suspense fallback={<Text text="Loading A" />}>
|
||||
<A />
|
||||
</Suspense>
|
||||
<Suspense fallback={<Text text="Loading B" />}>
|
||||
<Text text="B" />
|
||||
</Suspense>
|
||||
</SuspenseList>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
let abortStream;
|
||||
await serverAct(async () => {
|
||||
const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(<Foo />, {
|
||||
onError(error) {
|
||||
errors.push(error.message);
|
||||
},
|
||||
});
|
||||
pipe(writable);
|
||||
abortStream = abort;
|
||||
});
|
||||
|
||||
assertLog([
|
||||
'Suspend! [A]',
|
||||
'B', // TODO: Defer rendering the content after fallback if previous suspended,
|
||||
'Loading A',
|
||||
'Loading B',
|
||||
]);
|
||||
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
<div>
|
||||
<span>Loading A</span>
|
||||
<span>Loading B</span>
|
||||
</div>,
|
||||
);
|
||||
|
||||
await serverAct(() => {
|
||||
abortStream();
|
||||
});
|
||||
|
||||
expect(hasCompleted).toBe(true);
|
||||
expect(errors).toEqual([
|
||||
'The render was aborted by the server without a reason.',
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableSuspenseList
|
||||
it('can error a pending SuspenseList', async () => {
|
||||
const A = createAsyncText('A');
|
||||
|
||||
function Foo() {
|
||||
return (
|
||||
<div>
|
||||
<SuspenseList revealOrder="forwards">
|
||||
<Suspense fallback={<Text text="Loading A" />}>
|
||||
<A />
|
||||
</Suspense>
|
||||
<Suspense fallback={<Text text="Loading B" />}>
|
||||
<Text text="B" />
|
||||
</Suspense>
|
||||
</SuspenseList>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
await serverAct(async () => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />, {
|
||||
onError(error) {
|
||||
errors.push(error.message);
|
||||
},
|
||||
});
|
||||
pipe(writable);
|
||||
});
|
||||
|
||||
assertLog([
|
||||
'Suspend! [A]',
|
||||
'B', // TODO: Defer rendering the content after fallback if previous suspended,
|
||||
'Loading A',
|
||||
'Loading B',
|
||||
]);
|
||||
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
<div>
|
||||
<span>Loading A</span>
|
||||
<span>Loading B</span>
|
||||
</div>,
|
||||
);
|
||||
|
||||
await serverAct(async () => {
|
||||
A.reject(new Error('hi'));
|
||||
});
|
||||
|
||||
assertLog(['Error! [hi]']);
|
||||
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
<div>
|
||||
<span>Loading A</span>
|
||||
<span>B</span>
|
||||
</div>,
|
||||
);
|
||||
|
||||
expect(errors).toEqual(['hi']);
|
||||
expect(hasErrored).toBe(false);
|
||||
expect(hasCompleted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
41
packages/react-server/src/ReactFizzServer.js
vendored
41
packages/react-server/src/ReactFizzServer.js
vendored
|
|
@ -4392,6 +4392,14 @@ function erroredTask(
|
|||
encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, false);
|
||||
untrackBoundary(request, boundary);
|
||||
|
||||
const boundaryRow = boundary.row;
|
||||
if (boundaryRow !== null) {
|
||||
// Unblock the SuspenseListRow that was blocked by this boundary.
|
||||
if (--boundaryRow.pendingTasks === 0) {
|
||||
finishSuspenseListRow(request, boundaryRow);
|
||||
}
|
||||
}
|
||||
|
||||
// Regardless of what happens next, this boundary won't be displayed,
|
||||
// so we can flush it, if the parent already flushed.
|
||||
if (boundary.parentFlushed) {
|
||||
|
|
@ -4544,13 +4552,6 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
|||
segment.status = ABORTED;
|
||||
}
|
||||
|
||||
const row = task.row;
|
||||
if (row !== null) {
|
||||
if (--row.pendingTasks === 0) {
|
||||
finishSuspenseListRow(request, row);
|
||||
}
|
||||
}
|
||||
|
||||
const errorInfo = getThrownInfo(task.componentStack);
|
||||
|
||||
if (boundary === null) {
|
||||
|
|
@ -4573,7 +4574,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
|||
// we just need to mark it as postponed.
|
||||
logPostpone(request, postponeInstance.message, errorInfo, null);
|
||||
trackPostpone(request, trackedPostpones, task, segment);
|
||||
finishedTask(request, null, row, segment);
|
||||
finishedTask(request, null, task.row, segment);
|
||||
} else {
|
||||
const fatal = new Error(
|
||||
'The render was aborted with postpone when the shell is incomplete. Reason: ' +
|
||||
|
|
@ -4592,7 +4593,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
|||
// We log the error but we still resolve the prerender
|
||||
logRecoverableError(request, error, errorInfo, null);
|
||||
trackPostpone(request, trackedPostpones, task, segment);
|
||||
finishedTask(request, null, row, segment);
|
||||
finishedTask(request, null, task.row, segment);
|
||||
} else {
|
||||
logRecoverableError(request, error, errorInfo, null);
|
||||
fatalError(request, error, errorInfo, null);
|
||||
|
|
@ -4636,7 +4637,6 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
boundary.pendingTasks--;
|
||||
// We construct an errorInfo from the boundary's componentStack so the error in dev will indicate which
|
||||
// boundary the message is referring to
|
||||
const trackedPostpones = request.trackedPostpones;
|
||||
|
|
@ -4664,7 +4664,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
|||
abortTask(fallbackTask, request, error),
|
||||
);
|
||||
boundary.fallbackAbortableTasks.clear();
|
||||
return finishedTask(request, boundary, row, segment);
|
||||
return finishedTask(request, boundary, task.row, segment);
|
||||
}
|
||||
}
|
||||
boundary.status = CLIENT_RENDERED;
|
||||
|
|
@ -4681,7 +4681,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
|||
logPostpone(request, postponeInstance.message, errorInfo, null);
|
||||
if (request.trackedPostpones !== null && segment !== null) {
|
||||
trackPostpone(request, request.trackedPostpones, task, segment);
|
||||
finishedTask(request, task.blockedBoundary, row, segment);
|
||||
finishedTask(request, task.blockedBoundary, task.row, segment);
|
||||
|
||||
// If this boundary was still pending then we haven't already cancelled its fallbacks.
|
||||
// We'll need to abort the fallbacks, which will also error that parent boundary.
|
||||
|
|
@ -4706,6 +4706,16 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
|||
}
|
||||
}
|
||||
|
||||
boundary.pendingTasks--;
|
||||
|
||||
const boundaryRow = boundary.row;
|
||||
if (boundaryRow !== null) {
|
||||
// Unblock the SuspenseListRow that was blocked by this boundary.
|
||||
if (--boundaryRow.pendingTasks === 0) {
|
||||
finishSuspenseListRow(request, boundaryRow);
|
||||
}
|
||||
}
|
||||
|
||||
// If this boundary was still pending then we haven't already cancelled its fallbacks.
|
||||
// We'll need to abort the fallbacks, which will also error that parent boundary.
|
||||
boundary.fallbackAbortableTasks.forEach(fallbackTask =>
|
||||
|
|
@ -4714,6 +4724,13 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
|||
boundary.fallbackAbortableTasks.clear();
|
||||
}
|
||||
|
||||
const row = task.row;
|
||||
if (row !== null) {
|
||||
if (--row.pendingTasks === 0) {
|
||||
finishSuspenseListRow(request, row);
|
||||
}
|
||||
}
|
||||
|
||||
request.allPendingTasks--;
|
||||
if (request.allPendingTasks === 0) {
|
||||
completeAll(request);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user