[Flight] Aborting with a postpone instance as a reason should postpone remaining holes (#27576)
This lets you abort with postponing semantics.
Sebastian Markbåge committed
Oct 24, 2023 at 13:41 UTC
960ed6ea4341b4189a4f6c4edfa6bfe4c712e097
2 files changed
+77
-7
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+58
@@ -1309,4 +1309,62 @@ describe('ReactFlightDOMBrowser', () => {
1309
'The render was aborted by the server without a reason.',
1310
]);
1311
});
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
+ });
1370
});
packages/react-server/src/ReactFlightServer.js
+19
-7
@@ -1790,15 +1790,27 @@ export function abort(request: Request, reason: mixed): void {
1790
if (abortableTasks.size > 0) {
1791
// We have tasks to abort. We'll emit one error row and then emit a reference
1792
// 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);
1793
request.pendingChunks++;
1794
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
+ }
1814
abortableTasks.forEach(task => abortTask(task, request, errorId));
1815
abortableTasks.clear();
1816
}