Repository: facebook/react
Pull request: [Flight] Aborting with a postpone instance as a reason should postpone remaining holes

This lets you abort with postponing semantics.

Changed files:
filename: packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
patch: @@ -1309,4 +1309,62 @@ describe('ReactFlightDOMBrowser', () => {
       'The render was aborted by the server without a reason.',
     ]);
   });
+
+  // @gate enablePostpone
+  it('postpones when abort passes a postpone signal', async () => {
+    const infinitePromise = new Promise(() => {});
+    function Server() {
+      return infinitePromise;
+    }
+
+    let postponed = null;
+    let error = null;
+
+    const controller = new AbortController();
+    const stream = ReactServerDOMServer.renderToReadableStream(
+      <Suspense fallback="Loading...">
+        <Server />
+      </Suspense>,
+      null,
+      {
+        onError(x) {
+          error = x;
+        },
+        onPostpone(reason) {
+          postponed = reason;
+        },
+        signal: controller.signal,
+      },
+    );
+
+    try {
+      React.unstable_postpone('testing postpone');
+    } catch (reason) {
+      controller.abort(reason);
+    }
+
+    const response = ReactServerDOMClient.createFromReadableStream(stream);
+
+    function Client() {
+      return use(response);
+    }
+
+    const container = document.createElement('div');
+    const root = ReactDOMClient.createRoot(container);
+    await act(async () => {
+      root.render(
+        <div>
+          Shell: <Client />
+        </div>,
+      );
+    });
+    // We should have reserved the shell already. Which means that the Server
+    // Component should've been a lazy component.
+    expect(container.innerHTML).toContain('Shell:');
+    expect(container.innerHTML).toContain('Loading...');
+    expect(container.innerHTML).not.toContain('Not shown');
+
+    expect(postponed).toBe('testing postpone');
+    expect(error).toBe(null);
+  });
 });

filename: packages/react-server/src/ReactFlightServer.js
patch: @@ -1790,15 +1790,27 @@ export function abort(request: Request, reason: mixed): void {
     if (abortableTasks.size > 0) {
       // We have tasks to abort. We'll emit one error row and then emit a reference
       // to that row from every row that's still remaining.
-      const error =
-        reason === undefined
-          ? new Error('The render was aborted by the server without a reason.')
-          : reason;
-
-      const digest = logRecoverableError(request, error);
       request.pendingChunks++;
       const errorId = request.nextChunkId++;
-      emitErrorChunk(request, errorId, digest, error);
+      if (
+        enablePostpone &&
+        typeof reason === 'object' &&
+        reason !== null &&
+        (reason: any).$$typeof === REACT_POSTPONE_TYPE
+      ) {
+        const postponeInstance: Postpone = (reason: any);
+        logPostpone(request, postponeInstance.message);
+        emitPostponeChunk(request, errorId, postponeInstance);
+      } else {
+        const error =
+          reason === undefined
+            ? new Error(
+                'The render was aborted by the server without a reason.',
+              )
+            : reason;
+        const digest = logRecoverableError(request, error);
+        emitErrorChunk(request, errorId, digest, error);
+      }
       abortableTasks.forEach(task => abortTask(task, request, errorId));
       abortableTasks.clear();
     }