Skip to content

Commit 960ed6e

Browse files
authored
[Flight] Aborting with a postpone instance as a reason should postpone remaining holes (#27576)
This lets you abort with postponing semantics.
1 parent b8e47d9 commit 960ed6e

File tree

2 files changed

+77
-7
lines changed

2 files changed

+77
-7
lines changed

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,4 +1309,62 @@ describe('ReactFlightDOMBrowser', () => {
13091309
'The render was aborted by the server without a reason.',
13101310
]);
13111311
});
1312+
1313+
// @gate enablePostpone
1314+
it('postpones when abort passes a postpone signal', async () => {
1315+
const infinitePromise = new Promise(() => {});
1316+
function Server() {
1317+
return infinitePromise;
1318+
}
1319+
1320+
let postponed = null;
1321+
let error = null;
1322+
1323+
const controller = new AbortController();
1324+
const stream = ReactServerDOMServer.renderToReadableStream(
1325+
<Suspense fallback="Loading...">
1326+
<Server />
1327+
</Suspense>,
1328+
null,
1329+
{
1330+
onError(x) {
1331+
error = x;
1332+
},
1333+
onPostpone(reason) {
1334+
postponed = reason;
1335+
},
1336+
signal: controller.signal,
1337+
},
1338+
);
1339+
1340+
try {
1341+
React.unstable_postpone('testing postpone');
1342+
} catch (reason) {
1343+
controller.abort(reason);
1344+
}
1345+
1346+
const response = ReactServerDOMClient.createFromReadableStream(stream);
1347+
1348+
function Client() {
1349+
return use(response);
1350+
}
1351+
1352+
const container = document.createElement('div');
1353+
const root = ReactDOMClient.createRoot(container);
1354+
await act(async () => {
1355+
root.render(
1356+
<div>
1357+
Shell: <Client />
1358+
</div>,
1359+
);
1360+
});
1361+
// We should have reserved the shell already. Which means that the Server
1362+
// Component should've been a lazy component.
1363+
expect(container.innerHTML).toContain('Shell:');
1364+
expect(container.innerHTML).toContain('Loading...');
1365+
expect(container.innerHTML).not.toContain('Not shown');
1366+
1367+
expect(postponed).toBe('testing postpone');
1368+
expect(error).toBe(null);
1369+
});
13121370
});

packages/react-server/src/ReactFlightServer.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1790,15 +1790,27 @@ export function abort(request: Request, reason: mixed): void {
17901790
if (abortableTasks.size > 0) {
17911791
// We have tasks to abort. We'll emit one error row and then emit a reference
17921792
// to that row from every row that's still remaining.
1793-
const error =
1794-
reason === undefined
1795-
? new Error('The render was aborted by the server without a reason.')
1796-
: reason;
1797-
1798-
const digest = logRecoverableError(request, error);
17991793
request.pendingChunks++;
18001794
const errorId = request.nextChunkId++;
1801-
emitErrorChunk(request, errorId, digest, error);
1795+
if (
1796+
enablePostpone &&
1797+
typeof reason === 'object' &&
1798+
reason !== null &&
1799+
(reason: any).$$typeof === REACT_POSTPONE_TYPE
1800+
) {
1801+
const postponeInstance: Postpone = (reason: any);
1802+
logPostpone(request, postponeInstance.message);
1803+
emitPostponeChunk(request, errorId, postponeInstance);
1804+
} else {
1805+
const error =
1806+
reason === undefined
1807+
? new Error(
1808+
'The render was aborted by the server without a reason.',
1809+
)
1810+
: reason;
1811+
const digest = logRecoverableError(request, error);
1812+
emitErrorChunk(request, errorId, digest, error);
1813+
}
18021814
abortableTasks.forEach(task => abortTask(task, request, errorId));
18031815
abortableTasks.clear();
18041816
}

0 commit comments

Comments
 (0)